mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat: Refactor CLI tool configuration management and introduce skill context loader
- Updated `claude-cli-tools.ts` to support new model configurations and migration from older versions. - Added `getPredefinedModels` and `getAllPredefinedModels` functions for better model management. - Deprecated `cli-config-manager.ts` in favor of `claude-cli-tools.ts`, maintaining backward compatibility. - Introduced `skill-context-loader.ts` to handle skill context loading based on user prompts and keywords. - Enhanced tool configuration functions to include secondary models and improved migration logic. - Updated index file to register the new skill context loader tool.
This commit is contained in:
@@ -1,42 +1,53 @@
|
||||
{
|
||||
"$schema": "./cli-tools.schema.json",
|
||||
"version": "2.0.0",
|
||||
"version": "3.0.0",
|
||||
"models": {
|
||||
"gemini": ["gemini-2.5-pro", "gemini-2.5-flash" ],
|
||||
"qwen": ["coder-model", "vision-model" ],
|
||||
"codex": ["gpt-5.2"],
|
||||
"claude": ["sonnet", "opus", "haiku"],
|
||||
"opencode": [
|
||||
"opencode/glm-4.7-free",
|
||||
"opencode/gpt-5-nano",
|
||||
"opencode/grok-code",
|
||||
"opencode/minimax-m2.1-free",
|
||||
"anthropic/claude-sonnet-4-20250514",
|
||||
"anthropic/claude-opus-4-20250514",
|
||||
"openai/gpt-4.1",
|
||||
"openai/o3",
|
||||
"google/gemini-2.5-pro",
|
||||
"google/gemini-2.5-flash"
|
||||
]
|
||||
},
|
||||
"tools": {
|
||||
"gemini": {
|
||||
"enabled": true,
|
||||
"isBuiltin": true,
|
||||
"command": "gemini",
|
||||
"description": "Google AI for code analysis",
|
||||
"primaryModel": "gemini-2.5-pro",
|
||||
"secondaryModel": "gemini-2.5-flash",
|
||||
"tags": []
|
||||
},
|
||||
"qwen": {
|
||||
"enabled": true,
|
||||
"isBuiltin": true,
|
||||
"command": "qwen",
|
||||
"description": "Alibaba AI assistant",
|
||||
"primaryModel": "coder-model",
|
||||
"secondaryModel": "coder-model",
|
||||
"tags": []
|
||||
},
|
||||
"codex": {
|
||||
"enabled": true,
|
||||
"isBuiltin": true,
|
||||
"command": "codex",
|
||||
"description": "OpenAI code generation",
|
||||
"primaryModel": "gpt-5.2",
|
||||
"secondaryModel": "gpt-5.2",
|
||||
"tags": []
|
||||
},
|
||||
"claude": {
|
||||
"enabled": true,
|
||||
"isBuiltin": true,
|
||||
"command": "claude",
|
||||
"description": "Anthropic AI assistant",
|
||||
"primaryModel": "sonnet",
|
||||
"secondaryModel": "haiku",
|
||||
"tags": []
|
||||
},
|
||||
"opencode": {
|
||||
"enabled": true,
|
||||
"isBuiltin": true,
|
||||
"command": "opencode",
|
||||
"description": "OpenCode AI assistant",
|
||||
"primaryModel": "opencode/glm-4.7-free",
|
||||
"tags": []
|
||||
"secondaryModel": "opencode/glm-4.7-free",
|
||||
"tags": ["分析"]
|
||||
}
|
||||
},
|
||||
"customEndpoints": [
|
||||
@@ -46,5 +57,6 @@
|
||||
"enabled": true,
|
||||
"tags": []
|
||||
}
|
||||
]
|
||||
],
|
||||
"$schema": "./cli-tools.schema.json"
|
||||
}
|
||||
|
||||
@@ -159,8 +159,23 @@ async function execAction(toolName: string | undefined, jsonParams: string | und
|
||||
// Execute tool
|
||||
const result = await executeTool(toolName, params);
|
||||
|
||||
// Always output JSON
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
// Output raw result value for hooks, or JSON on error
|
||||
if (result.success && result.result !== undefined) {
|
||||
// For string results, output directly (useful for hooks)
|
||||
if (typeof result.result === 'string') {
|
||||
if (result.result) {
|
||||
console.log(result.result);
|
||||
}
|
||||
// Empty string = silent (no output)
|
||||
} else {
|
||||
// For object results, output JSON
|
||||
console.log(JSON.stringify(result.result, null, 2));
|
||||
}
|
||||
} else if (!result.success) {
|
||||
// Error case - output full JSON for debugging
|
||||
console.error(JSON.stringify(result, null, 2));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -308,3 +308,311 @@
|
||||
background: hsl(var(--destructive) / 0.1);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
* CLI Manager Split Layout (Claude Config)
|
||||
* ======================================== */
|
||||
|
||||
.cli-manager-split {
|
||||
display: flex;
|
||||
gap: var(--spacing-md, 1rem);
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.cli-manager-sidebar {
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
overflow-y: auto;
|
||||
background: hsl(var(--card));
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.cli-manager-sidebar h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.75rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.cli-manager-main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.25rem;
|
||||
background: hsl(var(--card));
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
/* Tool List Items */
|
||||
.cli-tool-list-item {
|
||||
padding: 0.625rem 0.75rem;
|
||||
cursor: pointer;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.cli-tool-list-item:hover {
|
||||
background: hsl(var(--accent));
|
||||
}
|
||||
|
||||
.cli-tool-list-item.selected {
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
border-left: 3px solid hsl(var(--primary));
|
||||
padding-left: calc(0.75rem - 3px);
|
||||
}
|
||||
|
||||
.cli-tool-list-item .tool-name {
|
||||
font-weight: 500;
|
||||
font-size: 0.8125rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.cli-tool-list-item .tool-status {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cli-tool-list-item .tool-status.installed {
|
||||
background: hsl(var(--success));
|
||||
}
|
||||
|
||||
.cli-tool-list-item .tool-status.not-installed {
|
||||
background: hsl(var(--muted-foreground) / 0.4);
|
||||
}
|
||||
|
||||
/* Config Mode Toggle */
|
||||
.config-mode-toggle {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.config-mode-btn {
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
background: transparent;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.config-mode-btn:hover {
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.config-mode-btn.active {
|
||||
background: hsl(var(--background));
|
||||
color: hsl(var(--primary));
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Tool Detail Header */
|
||||
.tool-detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.tool-detail-header h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Claude Settings Form */
|
||||
.claude-config-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.claude-config-form .form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.claude-config-form .form-group label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.claude-config-form .form-control {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
font-family: inherit;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.375rem;
|
||||
background: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.claude-config-form .form-control:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--primary));
|
||||
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.2);
|
||||
}
|
||||
|
||||
.claude-config-form .form-control::placeholder {
|
||||
color: hsl(var(--muted-foreground) / 0.6);
|
||||
}
|
||||
|
||||
/* Model Config Section */
|
||||
.model-config-section {
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.model-config-section h4 {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.75rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.model-config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.model-config-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.cli-manager-main .empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.875rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.cli-manager-main .empty-state i {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Endpoints List in Config Panel */
|
||||
.claude-endpoints-list {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.claude-endpoints-list h4 {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.75rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.claude-endpoint-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.625rem 0.75rem;
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.claude-endpoint-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.claude-endpoint-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.claude-endpoint-name {
|
||||
font-weight: 500;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.claude-endpoint-meta {
|
||||
font-size: 0.6875rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.claude-endpoint-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
/* Config Source Badge */
|
||||
.config-source-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 500;
|
||||
border-radius: 9999px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.config-source-badge.provider {
|
||||
background: hsl(var(--primary) / 0.15);
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.config-source-badge.direct {
|
||||
background: hsl(var(--success) / 0.15);
|
||||
color: hsl(var(--success));
|
||||
}
|
||||
|
||||
|
||||
@@ -2329,4 +2329,132 @@ select.cli-input {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ===========================
|
||||
Model Pool Detail Info Grid
|
||||
=========================== */
|
||||
|
||||
.provider-detail {
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.provider-detail .provider-detail-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.provider-detail .provider-detail-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.provider-detail .provider-detail-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.provider-detail .form-section {
|
||||
padding: 1.25rem;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.provider-detail .form-section h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.provider-detail .form-section p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--foreground));
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Info Grid - Used in Model Pool Detail */
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.info-item label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.info-item span {
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
/* Status badge inside info-item should not stretch */
|
||||
.info-item .status-badge {
|
||||
display: inline-flex;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Excluded Providers List */
|
||||
.excluded-providers-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.excluded-providers-list .tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.625rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.375rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Provider Actions in Detail Header */
|
||||
.provider-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.provider-detail .provider-detail-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.provider-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ let defaultCliTool = 'gemini';
|
||||
let promptConcatFormat = localStorage.getItem('ccw-prompt-format') || 'plain'; // plain, yaml, json
|
||||
let cliToolsConfig = {}; // CLI tools enable/disable config
|
||||
let apiEndpoints = []; // API endpoints from LiteLLM config
|
||||
let cliSettingsEndpoints = []; // CLI Settings endpoints (for Claude wrapper)
|
||||
|
||||
// Smart Context settings
|
||||
let smartContextEnabled = localStorage.getItem('ccw-smart-context') === 'true';
|
||||
@@ -43,10 +44,11 @@ async function loadAllStatuses() {
|
||||
semanticStatus = data.semantic || { available: false };
|
||||
ccwInstallStatus = data.ccwInstall || { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
|
||||
|
||||
// Load CLI tools config and API endpoints
|
||||
// Load CLI tools config, API endpoints, and CLI Settings
|
||||
await Promise.all([
|
||||
loadCliToolsConfig(),
|
||||
loadApiEndpoints()
|
||||
loadApiEndpoints(),
|
||||
loadCliSettingsEndpoints()
|
||||
]);
|
||||
|
||||
// Update badges
|
||||
@@ -285,6 +287,22 @@ async function loadApiEndpoints() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load CLI Settings endpoints (Claude wrapper configurations)
|
||||
*/
|
||||
async function loadCliSettingsEndpoints() {
|
||||
try {
|
||||
const response = await fetch('/api/cli/settings');
|
||||
if (!response.ok) return [];
|
||||
const data = await response.json();
|
||||
cliSettingsEndpoints = data.endpoints || [];
|
||||
return cliSettingsEndpoints;
|
||||
} catch (err) {
|
||||
console.error('Failed to load CLI settings endpoints:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Badge Update ==========
|
||||
function updateCliBadge() {
|
||||
const badge = document.getElementById('badgeCliTools');
|
||||
@@ -355,6 +373,52 @@ function renderCliStatus() {
|
||||
const isEnabled = config.enabled !== false;
|
||||
const canSetDefault = isAvailable && isEnabled && !isDefault;
|
||||
|
||||
// Special handling for Claude: show CLI Settings info
|
||||
const isClaude = tool === 'claude';
|
||||
const enabledCliSettings = isClaude ? cliSettingsEndpoints.filter(ep => ep.enabled) : [];
|
||||
const hasCliSettings = enabledCliSettings.length > 0;
|
||||
|
||||
// Build CLI Settings badge for Claude
|
||||
let cliSettingsBadge = '';
|
||||
if (isClaude && hasCliSettings) {
|
||||
cliSettingsBadge = `<span class="cli-tool-badge cli-settings-badge" title="${enabledCliSettings.length} endpoint(s) configured">${enabledCliSettings.length} Endpoint${enabledCliSettings.length > 1 ? 's' : ''}</span>`;
|
||||
}
|
||||
|
||||
// Build CLI Settings info for Claude
|
||||
let cliSettingsInfo = '';
|
||||
if (isClaude) {
|
||||
if (hasCliSettings) {
|
||||
const epNames = enabledCliSettings.slice(0, 2).map(ep => ep.name).join(', ');
|
||||
const moreCount = enabledCliSettings.length > 2 ? ` +${enabledCliSettings.length - 2}` : '';
|
||||
cliSettingsInfo = `
|
||||
<div class="cli-settings-info mt-2 p-2 rounded bg-muted/50 text-xs">
|
||||
<div class="flex items-center gap-1 text-muted-foreground mb-1">
|
||||
<i data-lucide="settings-2" class="w-3 h-3"></i>
|
||||
<span>CLI Wrapper Endpoints:</span>
|
||||
</div>
|
||||
<div class="text-foreground font-medium">${epNames}${moreCount}</div>
|
||||
<a href="#" onclick="navigateToApiSettings('cli-settings'); return false;" class="text-primary hover:underline mt-1 inline-flex items-center gap-1">
|
||||
<i data-lucide="external-link" class="w-3 h-3"></i>
|
||||
Configure
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
cliSettingsInfo = `
|
||||
<div class="cli-settings-info mt-2 p-2 rounded bg-muted/30 text-xs">
|
||||
<div class="flex items-center gap-1 text-muted-foreground">
|
||||
<i data-lucide="info" class="w-3 h-3"></i>
|
||||
<span>No CLI wrapper configured</span>
|
||||
</div>
|
||||
<a href="#" onclick="navigateToApiSettings('cli-settings'); return false;" class="text-primary hover:underline mt-1 inline-flex items-center gap-1">
|
||||
<i data-lucide="plus" class="w-3 h-3"></i>
|
||||
Add Endpoint
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="cli-tool-card tool-${tool} ${isAvailable ? 'available' : 'unavailable'} ${!isEnabled ? 'disabled' : ''}">
|
||||
<div class="cli-tool-header">
|
||||
@@ -362,6 +426,7 @@ function renderCliStatus() {
|
||||
<span class="cli-tool-name">${tool.charAt(0).toUpperCase() + tool.slice(1)}</span>
|
||||
${isDefault ? '<span class="cli-tool-badge">Default</span>' : ''}
|
||||
${!isEnabled && isAvailable ? '<span class="cli-tool-badge-disabled">Disabled</span>' : ''}
|
||||
${cliSettingsBadge}
|
||||
</div>
|
||||
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
|
||||
${toolDescriptions[tool]}
|
||||
@@ -376,6 +441,7 @@ function renderCliStatus() {
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
${cliSettingsInfo}
|
||||
<div class="cli-tool-actions mt-3 flex gap-2">
|
||||
${isAvailable ? (isEnabled
|
||||
? `<button class="btn-sm btn-outline-warning flex items-center gap-1" onclick="toggleCliTool('${tool}', false)">
|
||||
@@ -1323,3 +1389,39 @@ async function startSemanticInstall() {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Navigation ==========
|
||||
/**
|
||||
* Navigate to API Settings page with optional section
|
||||
* @param {string} section - Target section: 'cli-settings', 'providers', 'endpoints'
|
||||
*/
|
||||
function navigateToApiSettings(section) {
|
||||
// Try to switch to API Settings view
|
||||
if (typeof switchView === 'function') {
|
||||
switchView('api-settings');
|
||||
} else if (window.switchView) {
|
||||
window.switchView('api-settings');
|
||||
}
|
||||
|
||||
// After view switch, select the target section
|
||||
setTimeout(() => {
|
||||
if (section === 'cli-settings') {
|
||||
// Click CLI Settings tab if exists
|
||||
const cliSettingsTab = document.querySelector('[data-section="cli-settings"]');
|
||||
if (cliSettingsTab) {
|
||||
cliSettingsTab.click();
|
||||
} else {
|
||||
// Fallback: try to find and click by text content
|
||||
const tabs = document.querySelectorAll('.api-settings-sidebar .sidebar-item, .api-settings-tabs .tab-btn');
|
||||
tabs.forEach(tab => {
|
||||
if (tab.textContent.includes('CLI') || tab.textContent.includes('Wrapper')) {
|
||||
tab.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Export navigation function
|
||||
window.navigateToApiSettings = navigateToApiSettings;
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@ let poolDiscoveredProviders = {};
|
||||
// CLI Settings state
|
||||
let cliSettingsData = null;
|
||||
let selectedCliSettingsId = null;
|
||||
let cliConfigMode = 'provider'; // 'provider' | 'direct'
|
||||
let isAddingCliSettings = false;
|
||||
let editingCliSettingsId = null;
|
||||
|
||||
// Cache for ccw-litellm status (frontend cache with TTL)
|
||||
let ccwLitellmStatusCache = null;
|
||||
@@ -3711,14 +3714,21 @@ function renderCliSettingsList() {
|
||||
var html = '';
|
||||
endpoints.forEach(function(endpoint) {
|
||||
var isSelected = endpoint.id === selectedCliSettingsId;
|
||||
html += '<div class="provider-item' + (isSelected ? ' selected' : '') + '" onclick="selectCliSettings(\'' + endpoint.id + '\')">' +
|
||||
var isEditing = endpoint.id === editingCliSettingsId;
|
||||
var settings = endpoint.settings || {};
|
||||
var configMode = settings.configMode || (settings.providerId ? 'provider' : 'direct');
|
||||
var configIcon = configMode === 'provider' ? 'link' : 'key';
|
||||
|
||||
html += '<div class="provider-item' + (isSelected || isEditing ? ' selected' : '') + '" onclick="selectCliSettings(\'' + endpoint.id + '\')">' +
|
||||
'<div class="provider-item-content">' +
|
||||
'<div class="provider-icon">' +
|
||||
'<i data-lucide="settings"></i>' +
|
||||
'<i data-lucide="' + configIcon + '"></i>' +
|
||||
'</div>' +
|
||||
'<div class="provider-info">' +
|
||||
'<div class="provider-name">' + escapeHtml(endpoint.name) + '</div>' +
|
||||
'<div class="provider-type">' + (endpoint.settings.model || 'sonnet') + '</div>' +
|
||||
'<div class="provider-type" style="font-size: 0.75rem; color: var(--text-muted);">' +
|
||||
(configMode === 'provider' ? 'Provider' : 'Direct') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="provider-status' + (endpoint.enabled ? ' enabled' : ' disabled') + '">' +
|
||||
@@ -3759,10 +3769,45 @@ function renderCliSettingsDetail(endpointId) {
|
||||
|
||||
var settings = endpoint.settings || {};
|
||||
var env = settings.env || {};
|
||||
var configMode = settings.configMode || (settings.providerId ? 'provider' : 'direct');
|
||||
var configModeLabel = configMode === 'provider' ? (t('apiSettings.providerBinding') || 'Provider Binding') : (t('apiSettings.directConfig') || 'Direct Configuration');
|
||||
|
||||
// Build model config display
|
||||
var modelConfigHtml = '';
|
||||
if (env.ANTHROPIC_MODEL || env.ANTHROPIC_DEFAULT_HAIKU_MODEL || env.ANTHROPIC_DEFAULT_SONNET_MODEL || env.ANTHROPIC_DEFAULT_OPUS_MODEL) {
|
||||
modelConfigHtml =
|
||||
'<div class="detail-section">' +
|
||||
'<h3>' + (t('apiSettings.modelConfig') || 'Model Configuration') + '</h3>' +
|
||||
'<div class="detail-grid">' +
|
||||
'<div class="detail-item">' +
|
||||
'<label>ANTHROPIC_MODEL</label>' +
|
||||
'<span class="mono">' + escapeHtml(env.ANTHROPIC_MODEL || '-') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="detail-item">' +
|
||||
'<label>ANTHROPIC_DEFAULT_HAIKU_MODEL</label>' +
|
||||
'<span class="mono">' + escapeHtml(env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '-') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="detail-item">' +
|
||||
'<label>ANTHROPIC_DEFAULT_SONNET_MODEL</label>' +
|
||||
'<span class="mono">' + escapeHtml(env.ANTHROPIC_DEFAULT_SONNET_MODEL || '-') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="detail-item">' +
|
||||
'<label>ANTHROPIC_DEFAULT_OPUS_MODEL</label>' +
|
||||
'<span class="mono">' + escapeHtml(env.ANTHROPIC_DEFAULT_OPUS_MODEL || '-') + '</span>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
container.innerHTML =
|
||||
'<div class="provider-detail-header">' +
|
||||
'<div>' +
|
||||
'<h2>' + escapeHtml(endpoint.name) + '</h2>' +
|
||||
'<span class="config-source-badge ' + configMode + '" style="margin-top: 0.5rem; display: inline-block;">' +
|
||||
(configMode === 'provider' ? '<i data-lucide="link" style="width: 12px; height: 12px;"></i> ' : '<i data-lucide="key" style="width: 12px; height: 12px;"></i> ') +
|
||||
configModeLabel +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
'<div class="provider-detail-actions">' +
|
||||
'<button class="btn btn-ghost" onclick="editCliSettings(\'' + endpoint.id + '\')" title="' + t('common.edit') + '">' +
|
||||
'<i data-lucide="edit-2"></i>' +
|
||||
@@ -3781,15 +3826,12 @@ function renderCliSettingsDetail(endpointId) {
|
||||
'<span class="mono">' + escapeHtml(endpoint.id) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="detail-item">' +
|
||||
'<label>' + t('apiSettings.model') + '</label>' +
|
||||
'<span>' + escapeHtml(settings.model || 'sonnet') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="detail-item">' +
|
||||
'<label>' + t('apiSettings.status') + '</label>' +
|
||||
'<span class="status-badge ' + (endpoint.enabled ? 'enabled' : 'disabled') + '">' +
|
||||
(endpoint.enabled ? t('common.enabled') : t('common.disabled')) +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
(endpoint.description ? '<div class="detail-item detail-item-full"><label>' + t('apiSettings.description') + '</label><span>' + escapeHtml(endpoint.description) + '</span></div>' : '') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="detail-section">' +
|
||||
@@ -3805,6 +3847,7 @@ function renderCliSettingsDetail(endpointId) {
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
modelConfigHtml +
|
||||
'<div class="detail-section">' +
|
||||
'<h3>' + t('apiSettings.settingsFilePath') + '</h3>' +
|
||||
'<div class="code-block">' +
|
||||
@@ -3817,22 +3860,407 @@ function renderCliSettingsDetail(endpointId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render CLI Settings empty state
|
||||
* Render CLI Settings empty state or add form
|
||||
*/
|
||||
function renderCliSettingsEmptyState() {
|
||||
var container = document.getElementById('provider-detail-panel');
|
||||
if (!container) return;
|
||||
|
||||
// If adding new settings, show the form
|
||||
if (isAddingCliSettings) {
|
||||
renderCliSettingsForm(null);
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML =
|
||||
'<div class="provider-empty-state">' +
|
||||
'<i data-lucide="settings" class="empty-icon"></i>' +
|
||||
'<h3>' + t('apiSettings.noCliSettingsSelected') + '</h3>' +
|
||||
'<p>' + t('apiSettings.cliSettingsHint') + '</p>' +
|
||||
'<button class="btn btn-primary" style="margin-top: 1rem;" onclick="startAddCliSettings()">' +
|
||||
'<i data-lucide="plus"></i> ' + t('apiSettings.addCliSettings') +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start adding new CLI Settings (show form in panel)
|
||||
*/
|
||||
function startAddCliSettings() {
|
||||
isAddingCliSettings = true;
|
||||
selectedCliSettingsId = null;
|
||||
editingCliSettingsId = null;
|
||||
cliConfigMode = 'provider';
|
||||
renderCliSettingsForm(null);
|
||||
renderCliSettingsList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel adding/editing CLI Settings
|
||||
*/
|
||||
function cancelCliSettingsForm() {
|
||||
isAddingCliSettings = false;
|
||||
editingCliSettingsId = null;
|
||||
|
||||
// Re-render detail panel
|
||||
if (selectedCliSettingsId) {
|
||||
renderCliSettingsDetail(selectedCliSettingsId);
|
||||
} else {
|
||||
renderCliSettingsEmptyState();
|
||||
}
|
||||
renderCliSettingsList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render CLI Settings form in detail panel (for add or edit)
|
||||
*/
|
||||
function renderCliSettingsForm(existingEndpoint) {
|
||||
var container = document.getElementById('provider-detail-panel');
|
||||
if (!container) return;
|
||||
|
||||
var isEdit = !!existingEndpoint;
|
||||
var settings = existingEndpoint ? existingEndpoint.settings : { env: {}, model: '' };
|
||||
var env = settings.env || {};
|
||||
|
||||
// Determine initial config mode for editing
|
||||
if (isEdit) {
|
||||
// If settings has configMode, use it; otherwise detect based on providerId
|
||||
cliConfigMode = settings.configMode || (settings.providerId ? 'provider' : 'direct');
|
||||
}
|
||||
|
||||
// Build mode toggle
|
||||
var modeToggleHtml =
|
||||
'<div class="config-mode-toggle">' +
|
||||
'<button type="button" class="config-mode-btn' + (cliConfigMode === 'provider' ? ' active' : '') + '" data-mode="provider" onclick="switchCliConfigMode(\'provider\')">' +
|
||||
'<i data-lucide="link"></i> ' + (t('apiSettings.providerBinding') || 'Provider Binding') +
|
||||
'</button>' +
|
||||
'<button type="button" class="config-mode-btn' + (cliConfigMode === 'direct' ? ' active' : '') + '" data-mode="direct" onclick="switchCliConfigMode(\'direct\')">' +
|
||||
'<i data-lucide="key"></i> ' + (t('apiSettings.directConfig') || 'Direct Configuration') +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
|
||||
// Common fields
|
||||
var commonFieldsHtml =
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-settings-name">' + t('apiSettings.endpointName') + ' *</label>' +
|
||||
'<input type="text" id="cli-settings-name" class="form-control" value="' + escapeHtml(existingEndpoint ? existingEndpoint.name : '') + '" placeholder="My Claude Endpoint" required />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-settings-description">' + t('apiSettings.description') + '</label>' +
|
||||
'<input type="text" id="cli-settings-description" class="form-control" value="' + escapeHtml(existingEndpoint ? (existingEndpoint.description || '') : '') + '" placeholder="Optional description" />' +
|
||||
'</div>';
|
||||
|
||||
// Mode-specific form content container
|
||||
var formContentHtml = '<div id="cli-config-mode-content"></div>';
|
||||
|
||||
// Enabled toggle
|
||||
var enabledHtml =
|
||||
'<div class="form-group" style="margin-top: 1rem;">' +
|
||||
'<label class="checkbox-label">' +
|
||||
'<input type="checkbox" id="cli-settings-enabled"' + (existingEndpoint ? (existingEndpoint.enabled ? ' checked' : '') : ' checked') + ' />' +
|
||||
' ' + t('common.enabled') +
|
||||
'</label>' +
|
||||
'</div>';
|
||||
|
||||
// Action buttons
|
||||
var actionsHtml =
|
||||
'<div class="form-actions" style="margin-top: 1.5rem; display: flex; gap: 0.75rem; justify-content: flex-end;">' +
|
||||
'<button type="button" class="btn btn-secondary" onclick="cancelCliSettingsForm()">' + t('common.cancel') + '</button>' +
|
||||
'<button type="button" class="btn btn-primary" onclick="submitCliSettingsForm()">' +
|
||||
'<i data-lucide="save"></i> ' + (isEdit ? t('common.save') : t('common.create')) +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
|
||||
container.innerHTML =
|
||||
'<div class="tool-detail-header">' +
|
||||
'<h3><i data-lucide="settings"></i> ' + (isEdit ? t('apiSettings.editCliSettings') : t('apiSettings.addCliSettings')) + '</h3>' +
|
||||
'</div>' +
|
||||
'<div class="claude-config-form">' +
|
||||
(isEdit ? '<input type="hidden" id="cli-settings-id" value="' + existingEndpoint.id + '">' : '') +
|
||||
'<input type="hidden" id="cli-config-mode" value="' + cliConfigMode + '">' +
|
||||
modeToggleHtml +
|
||||
commonFieldsHtml +
|
||||
formContentHtml +
|
||||
enabledHtml +
|
||||
actionsHtml +
|
||||
'</div>';
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
|
||||
// Render mode-specific content
|
||||
renderCliConfigModeContent(existingEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch CLI config mode
|
||||
*/
|
||||
function switchCliConfigMode(mode) {
|
||||
cliConfigMode = mode;
|
||||
|
||||
// Update hidden input
|
||||
var modeInput = document.getElementById('cli-config-mode');
|
||||
if (modeInput) modeInput.value = mode;
|
||||
|
||||
// Update toggle buttons
|
||||
document.querySelectorAll('.config-mode-btn').forEach(function(btn) {
|
||||
btn.classList.toggle('active', btn.dataset.mode === mode);
|
||||
});
|
||||
|
||||
// Re-render mode content while preserving form data
|
||||
var existingEndpoint = null;
|
||||
var idInput = document.getElementById('cli-settings-id');
|
||||
if (idInput && idInput.value && cliSettingsData && cliSettingsData.endpoints) {
|
||||
existingEndpoint = cliSettingsData.endpoints.find(function(e) { return e.id === idInput.value; });
|
||||
}
|
||||
|
||||
renderCliConfigModeContent(existingEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render CLI config mode-specific content
|
||||
*/
|
||||
function renderCliConfigModeContent(existingEndpoint) {
|
||||
var container = document.getElementById('cli-config-mode-content');
|
||||
if (!container) return;
|
||||
|
||||
var settings = existingEndpoint ? existingEndpoint.settings : { env: {}, model: '' };
|
||||
var env = settings.env || {};
|
||||
|
||||
if (cliConfigMode === 'provider') {
|
||||
renderProviderModeContent(container, settings);
|
||||
} else {
|
||||
renderDirectModeContent(container, env);
|
||||
}
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Provider Binding mode content
|
||||
*/
|
||||
function renderProviderModeContent(container, settings) {
|
||||
var providers = getAvailableAnthropicProviders();
|
||||
var hasProviders = providers.length > 0;
|
||||
var selectedProviderId = settings.providerId || '';
|
||||
var providerOptionsHtml = buildCliProviderOptions(selectedProviderId);
|
||||
var env = settings.env || {};
|
||||
|
||||
var noProvidersWarning = !hasProviders ?
|
||||
'<div class="info-message" style="margin-bottom: 1rem; padding: 0.75rem; background: hsl(var(--warning) / 0.1); border-radius: 0.375rem; display: flex; align-items: center; gap: 0.5rem;">' +
|
||||
'<i data-lucide="alert-circle" style="width: 16px; height: 16px; color: hsl(var(--warning));"></i>' +
|
||||
'<span style="font-size: 0.8125rem;">' + (t('apiSettings.noAnthropicProviders') || 'No Anthropic providers configured. Please add a provider first.') + '</span>' +
|
||||
'</div>' : '';
|
||||
|
||||
container.innerHTML = noProvidersWarning +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-settings-provider">' + t('apiSettings.provider') + ' *</label>' +
|
||||
'<select id="cli-settings-provider" class="form-control" onchange="onCliProviderChange()"' + (!hasProviders ? ' disabled' : '') + '>' +
|
||||
providerOptionsHtml +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
// Model Config Section
|
||||
'<div class="model-config-section">' +
|
||||
'<h4><i data-lucide="cpu"></i> ' + (t('apiSettings.modelConfig') || 'Model Configuration') + '</h4>' +
|
||||
'<div class="model-config-grid">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-model-default">ANTHROPIC_MODEL</label>' +
|
||||
'<input type="text" id="cli-model-default" class="form-control" placeholder="claude-3-5-sonnet-20241022" value="' + escapeHtml(env.ANTHROPIC_MODEL || '') + '" />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-model-haiku">ANTHROPIC_DEFAULT_HAIKU_MODEL</label>' +
|
||||
'<input type="text" id="cli-model-haiku" class="form-control" placeholder="claude-3-haiku-20240307" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '') + '" />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-model-sonnet">ANTHROPIC_DEFAULT_SONNET_MODEL</label>' +
|
||||
'<input type="text" id="cli-model-sonnet" class="form-control" placeholder="claude-3-5-sonnet-20241022" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_SONNET_MODEL || '') + '" />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-model-opus">ANTHROPIC_DEFAULT_OPUS_MODEL</label>' +
|
||||
'<input type="text" id="cli-model-opus" class="form-control" placeholder="claude-3-opus-20240229" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_OPUS_MODEL || '') + '" />' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Direct Configuration mode content
|
||||
*/
|
||||
function renderDirectModeContent(container, env) {
|
||||
container.innerHTML =
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-auth-token">ANTHROPIC_AUTH_TOKEN *</label>' +
|
||||
'<input type="password" id="cli-auth-token" class="form-control" placeholder="sk-ant-..." value="' + escapeHtml(env.ANTHROPIC_AUTH_TOKEN || '') + '" />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-base-url">ANTHROPIC_BASE_URL</label>' +
|
||||
'<input type="text" id="cli-base-url" class="form-control" placeholder="https://api.anthropic.com (optional)" value="' + escapeHtml(env.ANTHROPIC_BASE_URL || '') + '" />' +
|
||||
'</div>' +
|
||||
// Model Config Section
|
||||
'<div class="model-config-section">' +
|
||||
'<h4><i data-lucide="cpu"></i> ' + (t('apiSettings.modelConfig') || 'Model Configuration') + '</h4>' +
|
||||
'<div class="model-config-grid">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-model-default">ANTHROPIC_MODEL</label>' +
|
||||
'<input type="text" id="cli-model-default" class="form-control" placeholder="claude-3-5-sonnet-20241022" value="' + escapeHtml(env.ANTHROPIC_MODEL || '') + '" />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-model-haiku">ANTHROPIC_DEFAULT_HAIKU_MODEL</label>' +
|
||||
'<input type="text" id="cli-model-haiku" class="form-control" placeholder="claude-3-haiku-20240307" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '') + '" />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-model-sonnet">ANTHROPIC_DEFAULT_SONNET_MODEL</label>' +
|
||||
'<input type="text" id="cli-model-sonnet" class="form-control" placeholder="claude-3-5-sonnet-20241022" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_SONNET_MODEL || '') + '" />' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="cli-model-opus">ANTHROPIC_DEFAULT_OPUS_MODEL</label>' +
|
||||
'<input type="text" id="cli-model-opus" class="form-control" placeholder="claude-3-opus-20240229" value="' + escapeHtml(env.ANTHROPIC_DEFAULT_OPUS_MODEL || '') + '" />' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit CLI Settings Form (handles both Provider and Direct modes)
|
||||
*/
|
||||
async function submitCliSettingsForm() {
|
||||
// Get common fields
|
||||
var name = document.getElementById('cli-settings-name').value.trim();
|
||||
var description = document.getElementById('cli-settings-description').value.trim();
|
||||
var enabled = document.getElementById('cli-settings-enabled').checked;
|
||||
var idInput = document.getElementById('cli-settings-id');
|
||||
var id = idInput ? idInput.value : null;
|
||||
var configMode = cliConfigMode;
|
||||
|
||||
// Get model configuration fields
|
||||
var anthropicModel = document.getElementById('cli-model-default').value.trim();
|
||||
var haikuModel = document.getElementById('cli-model-haiku').value.trim();
|
||||
var sonnetModel = document.getElementById('cli-model-sonnet').value.trim();
|
||||
var opusModel = document.getElementById('cli-model-opus').value.trim();
|
||||
|
||||
// Validate common fields
|
||||
if (!name) {
|
||||
showRefreshToast(t('apiSettings.nameRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var data = {
|
||||
name: name,
|
||||
description: description,
|
||||
enabled: enabled,
|
||||
settings: {
|
||||
env: {
|
||||
DISABLE_AUTOUPDATER: '1'
|
||||
},
|
||||
configMode: configMode,
|
||||
includeCoAuthoredBy: false
|
||||
}
|
||||
};
|
||||
|
||||
// Mode-specific handling
|
||||
if (configMode === 'provider') {
|
||||
// Provider binding mode
|
||||
var providerId = document.getElementById('cli-settings-provider').value;
|
||||
|
||||
if (!providerId) {
|
||||
showRefreshToast(t('apiSettings.providerRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get provider credentials
|
||||
var providers = getAvailableAnthropicProviders();
|
||||
var provider = providers.find(function(p) { return p.id === providerId; });
|
||||
|
||||
if (!provider) {
|
||||
showRefreshToast(t('apiSettings.providerNotFound'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy provider credentials to env
|
||||
data.settings.env.ANTHROPIC_AUTH_TOKEN = provider.apiKey || '';
|
||||
if (provider.apiBase) {
|
||||
data.settings.env.ANTHROPIC_BASE_URL = provider.apiBase;
|
||||
}
|
||||
data.settings.providerId = providerId;
|
||||
|
||||
} else {
|
||||
// Direct configuration mode
|
||||
var authToken = document.getElementById('cli-auth-token').value.trim();
|
||||
var baseUrl = document.getElementById('cli-base-url').value.trim();
|
||||
|
||||
if (!authToken) {
|
||||
showRefreshToast(t('apiSettings.authTokenRequired') || 'Auth token is required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
data.settings.env.ANTHROPIC_AUTH_TOKEN = authToken;
|
||||
if (baseUrl) {
|
||||
data.settings.env.ANTHROPIC_BASE_URL = baseUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Add model configuration
|
||||
if (anthropicModel) {
|
||||
data.settings.env.ANTHROPIC_MODEL = anthropicModel;
|
||||
}
|
||||
if (haikuModel) {
|
||||
data.settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = haikuModel;
|
||||
}
|
||||
if (sonnetModel) {
|
||||
data.settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL = sonnetModel;
|
||||
}
|
||||
if (opusModel) {
|
||||
data.settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL = opusModel;
|
||||
}
|
||||
|
||||
// Set ID if editing
|
||||
if (id) {
|
||||
data.id = id;
|
||||
}
|
||||
|
||||
// Save endpoint
|
||||
var result = await saveCliSettingsEndpoint(data);
|
||||
if (result && result.success) {
|
||||
// Reset form state
|
||||
isAddingCliSettings = false;
|
||||
editingCliSettingsId = null;
|
||||
|
||||
// Select the newly created/updated endpoint
|
||||
if (result.endpoint && result.endpoint.id) {
|
||||
selectedCliSettingsId = result.endpoint.id;
|
||||
}
|
||||
|
||||
// Refresh view
|
||||
await loadCliSettings();
|
||||
renderCliSettingsList();
|
||||
if (selectedCliSettingsId) {
|
||||
renderCliSettingsDetail(selectedCliSettingsId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit CLI Settings in panel (new panel-based approach)
|
||||
*/
|
||||
function editCliSettingsInPanel(endpointId) {
|
||||
var endpoint = null;
|
||||
if (cliSettingsData && cliSettingsData.endpoints) {
|
||||
endpoint = cliSettingsData.endpoints.find(function(e) { return e.id === endpointId; });
|
||||
}
|
||||
if (endpoint) {
|
||||
isAddingCliSettings = false;
|
||||
editingCliSettingsId = endpointId;
|
||||
|
||||
// Determine config mode from existing settings
|
||||
var settings = endpoint.settings || {};
|
||||
cliConfigMode = settings.configMode || (settings.providerId ? 'provider' : 'direct');
|
||||
|
||||
renderCliSettingsForm(endpoint);
|
||||
renderCliSettingsList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available Anthropic providers
|
||||
*/
|
||||
@@ -3964,16 +4392,10 @@ function showAddCliSettingsModal(existingEndpoint) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit CLI Settings
|
||||
* Edit CLI Settings (uses panel-based form)
|
||||
*/
|
||||
function editCliSettings(endpointId) {
|
||||
var endpoint = null;
|
||||
if (cliSettingsData && cliSettingsData.endpoints) {
|
||||
endpoint = cliSettingsData.endpoints.find(function(e) { return e.id === endpointId; });
|
||||
}
|
||||
if (endpoint) {
|
||||
showAddCliSettingsModal(endpoint);
|
||||
}
|
||||
editCliSettingsInPanel(endpointId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4381,8 +4803,150 @@ async function submitModelPool(event) {
|
||||
* Edit model pool
|
||||
*/
|
||||
function editModelPool(poolId) {
|
||||
// TODO: Implement edit modal
|
||||
showRefreshToast('Edit functionality coming soon', 'info');
|
||||
var pool = modelPools.find(function(p) { return p.id === poolId; });
|
||||
if (!pool) {
|
||||
showRefreshToast(t('common.error') + ': Pool not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var modalHtml = '<div class="generic-modal-overlay active" id="edit-pool-modal">' +
|
||||
'<div class="generic-modal" style="max-width: 600px;">' +
|
||||
'<div class="generic-modal-header">' +
|
||||
'<h3 class="generic-modal-title">' + t('apiSettings.editModelPool') + '</h3>' +
|
||||
'<button class="generic-modal-close" onclick="closeEditPoolModal()">×</button>' +
|
||||
'</div>' +
|
||||
'<div class="generic-modal-body">' +
|
||||
'<form id="edit-pool-form" class="api-settings-form" onsubmit="submitEditModelPool(event, \'' + poolId + '\')">' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.modelType') + '</label>' +
|
||||
'<input type="text" class="cli-input" value="' + (pool.modelType === 'embedding' ? 'Embedding' : pool.modelType === 'llm' ? 'LLM' : 'Reranker') + '" disabled />' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.poolName') + '</label>' +
|
||||
'<input type="text" id="edit-pool-name" class="cli-input" value="' + escapeHtml(pool.name || '') + '" placeholder="e.g., Primary Embedding Pool" />' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.targetModel') + '</label>' +
|
||||
'<input type="text" class="cli-input" value="' + escapeHtml(pool.targetModel) + '" disabled />' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.strategy') + ' *</label>' +
|
||||
'<select id="edit-pool-strategy" class="cli-input" required>' +
|
||||
'<option value="round_robin"' + (pool.strategy === 'round_robin' ? ' selected' : '') + '>Round Robin</option>' +
|
||||
'<option value="latency_aware"' + (pool.strategy === 'latency_aware' ? ' selected' : '') + '>Latency Aware</option>' +
|
||||
'<option value="weighted_random"' + (pool.strategy === 'weighted_random' ? ' selected' : '') + '>Weighted Random</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.cooldown') + ' (seconds)</label>' +
|
||||
'<input type="number" id="edit-pool-cooldown" class="cli-input" value="' + (pool.defaultCooldown || 60) + '" min="0" />' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.maxConcurrent') + '</label>' +
|
||||
'<input type="number" id="edit-pool-max-concurrent" class="cli-input" value="' + (pool.defaultMaxConcurrentPerKey || 4) + '" min="1" />' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label>' + t('apiSettings.description') + '</label>' +
|
||||
'<textarea id="edit-pool-description" class="cli-input" rows="2" placeholder="Optional description">' + escapeHtml(pool.description || '') + '</textarea>' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label class="checkbox-label">' +
|
||||
'<input type="checkbox" id="edit-pool-enabled"' + (pool.enabled ? ' checked' : '') + ' /> ' + t('apiSettings.enablePool') +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label class="checkbox-label">' +
|
||||
'<input type="checkbox" id="edit-pool-auto-discover"' + (pool.autoDiscover !== false ? ' checked' : '') + ' /> ' + t('apiSettings.autoDiscoverProviders') +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
|
||||
'</form>' +
|
||||
'</div>' +
|
||||
'<div class="generic-modal-footer">' +
|
||||
'<button class="btn btn-secondary" onclick="closeEditPoolModal()">' + t('common.cancel') + '</button>' +
|
||||
'<button class="btn btn-primary" onclick="document.getElementById(\'edit-pool-form\').requestSubmit()">' + t('common.save') + '</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close edit pool modal
|
||||
*/
|
||||
function closeEditPoolModal() {
|
||||
var modal = document.getElementById('edit-pool-modal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit edit model pool form
|
||||
*/
|
||||
async function submitEditModelPool(event, poolId) {
|
||||
event.preventDefault();
|
||||
|
||||
var pool = modelPools.find(function(p) { return p.id === poolId; });
|
||||
if (!pool) {
|
||||
showRefreshToast(t('common.error') + ': Pool not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var name = document.getElementById('edit-pool-name').value.trim();
|
||||
var strategy = document.getElementById('edit-pool-strategy').value;
|
||||
var cooldown = parseInt(document.getElementById('edit-pool-cooldown').value || '60');
|
||||
var maxConcurrent = parseInt(document.getElementById('edit-pool-max-concurrent').value || '4');
|
||||
var description = document.getElementById('edit-pool-description').value.trim();
|
||||
var enabled = document.getElementById('edit-pool-enabled').checked;
|
||||
var autoDiscover = document.getElementById('edit-pool-auto-discover').checked;
|
||||
|
||||
var poolData = {
|
||||
id: poolId,
|
||||
modelType: pool.modelType,
|
||||
enabled: enabled,
|
||||
name: name || pool.targetModel,
|
||||
targetModel: pool.targetModel,
|
||||
strategy: strategy,
|
||||
autoDiscover: autoDiscover,
|
||||
defaultCooldown: cooldown,
|
||||
defaultMaxConcurrentPerKey: maxConcurrent,
|
||||
description: description || undefined,
|
||||
excludedProviderIds: pool.excludedProviderIds || []
|
||||
};
|
||||
|
||||
try {
|
||||
await initCsrfToken();
|
||||
var response = await csrfFetch('/api/litellm-api/model-pools/' + poolId, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(poolData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
var err = await response.json();
|
||||
throw new Error(err.error || 'Failed to update pool');
|
||||
}
|
||||
|
||||
showRefreshToast(t('apiSettings.poolUpdated'), 'success');
|
||||
closeEditPoolModal();
|
||||
|
||||
// Reload pools and refresh view
|
||||
await loadModelPools();
|
||||
renderModelPoolsList();
|
||||
renderModelPoolDetail(poolId);
|
||||
} catch (err) {
|
||||
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4426,6 +4990,8 @@ window.closeAddPoolModal = closeAddPoolModal;
|
||||
window.onPoolModelTypeChange = onPoolModelTypeChange;
|
||||
window.submitModelPool = submitModelPool;
|
||||
window.editModelPool = editModelPool;
|
||||
window.closeEditPoolModal = closeEditPoolModal;
|
||||
window.submitEditModelPool = submitEditModelPool;
|
||||
window.deleteModelPool = deleteModelPool;
|
||||
|
||||
// Make CLI Settings functions globally accessible
|
||||
@@ -4441,6 +5007,12 @@ window.editCliSettings = editCliSettings;
|
||||
window.closeCliSettingsModal = closeCliSettingsModal;
|
||||
window.submitCliSettings = submitCliSettings;
|
||||
window.onCliProviderChange = onCliProviderChange;
|
||||
// New panel-based CLI Settings functions
|
||||
window.startAddCliSettings = startAddCliSettings;
|
||||
window.cancelCliSettingsForm = cancelCliSettingsForm;
|
||||
window.switchCliConfigMode = switchCliConfigMode;
|
||||
window.submitCliSettingsForm = submitCliSettingsForm;
|
||||
window.editCliSettingsInPanel = editCliSettingsInPanel;
|
||||
|
||||
|
||||
// ========== Utility Functions ==========
|
||||
|
||||
@@ -1,6 +1,75 @@
|
||||
// CodexLens Manager - Configuration, Model Management, and Semantic Dependencies
|
||||
// Extracted from cli-manager.js for better maintainability
|
||||
|
||||
// ============================================================
|
||||
// CACHE MANAGEMENT
|
||||
// ============================================================
|
||||
|
||||
// Cache TTL in milliseconds (30 seconds default)
|
||||
const CODEXLENS_CACHE_TTL = 30000;
|
||||
|
||||
// Cache storage for CodexLens data
|
||||
const codexLensCache = {
|
||||
workspaceStatus: { data: null, timestamp: 0 },
|
||||
config: { data: null, timestamp: 0 },
|
||||
status: { data: null, timestamp: 0 },
|
||||
env: { data: null, timestamp: 0 },
|
||||
models: { data: null, timestamp: 0 },
|
||||
rerankerModels: { data: null, timestamp: 0 },
|
||||
semanticStatus: { data: null, timestamp: 0 },
|
||||
gpuList: { data: null, timestamp: 0 },
|
||||
indexes: { data: null, timestamp: 0 }
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if cache is valid (not expired)
|
||||
* @param {string} key - Cache key
|
||||
* @param {number} ttl - Optional custom TTL
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isCacheValid(key, ttl = CODEXLENS_CACHE_TTL) {
|
||||
const cache = codexLensCache[key];
|
||||
if (!cache || !cache.data) return false;
|
||||
return (Date.now() - cache.timestamp) < ttl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached data
|
||||
* @param {string} key - Cache key
|
||||
* @returns {*} Cached data or null
|
||||
*/
|
||||
function getCachedData(key) {
|
||||
return codexLensCache[key]?.data || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cache data
|
||||
* @param {string} key - Cache key
|
||||
* @param {*} data - Data to cache
|
||||
*/
|
||||
function setCacheData(key, data) {
|
||||
if (codexLensCache[key]) {
|
||||
codexLensCache[key].data = data;
|
||||
codexLensCache[key].timestamp = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate specific cache or all caches
|
||||
* @param {string} key - Cache key (optional, if not provided clears all)
|
||||
*/
|
||||
function invalidateCache(key) {
|
||||
if (key && codexLensCache[key]) {
|
||||
codexLensCache[key].data = null;
|
||||
codexLensCache[key].timestamp = 0;
|
||||
} else if (!key) {
|
||||
Object.keys(codexLensCache).forEach(function(k) {
|
||||
codexLensCache[k].data = null;
|
||||
codexLensCache[k].timestamp = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ============================================================
|
||||
@@ -25,8 +94,9 @@ function escapeHtml(str) {
|
||||
/**
|
||||
* Refresh workspace index status (FTS and Vector coverage)
|
||||
* Updates both the detailed panel (if exists) and header badges
|
||||
* @param {boolean} forceRefresh - Force refresh, bypass cache
|
||||
*/
|
||||
async function refreshWorkspaceIndexStatus() {
|
||||
async function refreshWorkspaceIndexStatus(forceRefresh) {
|
||||
var container = document.getElementById('workspaceIndexStatusContent');
|
||||
var headerFtsEl = document.getElementById('headerFtsPercent');
|
||||
var headerVectorEl = document.getElementById('headerVectorPercent');
|
||||
@@ -34,6 +104,13 @@ async function refreshWorkspaceIndexStatus() {
|
||||
// If neither container nor header elements exist, nothing to update
|
||||
if (!container && !headerFtsEl) return;
|
||||
|
||||
// Check cache first (unless force refresh)
|
||||
if (!forceRefresh && isCacheValid('workspaceStatus')) {
|
||||
var cachedResult = getCachedData('workspaceStatus');
|
||||
updateWorkspaceStatusUI(cachedResult, container, headerFtsEl, headerVectorEl);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state in container
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="text-xs text-muted-foreground text-center py-2">' +
|
||||
@@ -46,106 +123,10 @@ async function refreshWorkspaceIndexStatus() {
|
||||
var response = await fetch('/api/codexlens/workspace-status');
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
var ftsPercent = result.hasIndex ? (result.fts.percent || 0) : 0;
|
||||
var vectorPercent = result.hasIndex ? (result.vector.percent || 0) : 0;
|
||||
// Cache the result
|
||||
setCacheData('workspaceStatus', result);
|
||||
|
||||
// Update header badges (always update if elements exist)
|
||||
if (headerFtsEl) {
|
||||
headerFtsEl.textContent = ftsPercent + '%';
|
||||
headerFtsEl.className = 'text-sm font-medium ' +
|
||||
(ftsPercent >= 100 ? 'text-success' : (ftsPercent > 0 ? 'text-blue-500' : 'text-muted-foreground'));
|
||||
}
|
||||
if (headerVectorEl) {
|
||||
headerVectorEl.textContent = vectorPercent.toFixed(1) + '%';
|
||||
headerVectorEl.className = 'text-sm font-medium ' +
|
||||
(vectorPercent >= 100 ? 'text-success' : (vectorPercent >= 50 ? 'text-purple-500' : (vectorPercent > 0 ? 'text-purple-400' : 'text-muted-foreground')));
|
||||
}
|
||||
|
||||
// Update detailed container (if exists)
|
||||
if (container) {
|
||||
var html = '';
|
||||
|
||||
if (!result.hasIndex) {
|
||||
// No index for current workspace
|
||||
html = '<div class="text-center py-3">' +
|
||||
'<div class="text-sm text-muted-foreground mb-2">' +
|
||||
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
|
||||
(t('codexlens.noIndexFound') || 'No index found for current workspace') +
|
||||
'</div>' +
|
||||
'<button onclick="runFtsFullIndex()" class="text-xs text-primary hover:underline">' +
|
||||
(t('codexlens.createIndex') || 'Create Index') +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
} else {
|
||||
// FTS Status
|
||||
var ftsColor = ftsPercent >= 100 ? 'bg-success' : (ftsPercent > 0 ? 'bg-blue-500' : 'bg-muted-foreground');
|
||||
var ftsTextColor = ftsPercent >= 100 ? 'text-success' : (ftsPercent > 0 ? 'text-blue-500' : 'text-muted-foreground');
|
||||
|
||||
html += '<div class="space-y-1">' +
|
||||
'<div class="flex items-center justify-between text-xs">' +
|
||||
'<span class="flex items-center gap-1.5">' +
|
||||
'<i data-lucide="file-text" class="w-3.5 h-3.5 text-blue-500"></i> ' +
|
||||
'<span class="font-medium">' + (t('codexlens.ftsIndex') || 'FTS Index') + '</span>' +
|
||||
'</span>' +
|
||||
'<span class="' + ftsTextColor + ' font-medium">' + ftsPercent + '%</span>' +
|
||||
'</div>' +
|
||||
'<div class="h-1.5 bg-muted rounded-full overflow-hidden">' +
|
||||
'<div class="h-full ' + ftsColor + ' transition-all duration-300" style="width: ' + ftsPercent + '%"></div>' +
|
||||
'</div>' +
|
||||
'<div class="text-xs text-muted-foreground">' +
|
||||
(result.fts.indexedFiles || 0) + ' / ' + (result.fts.totalFiles || 0) + ' ' + (t('codexlens.filesIndexed') || 'files indexed') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Vector Status
|
||||
var vectorColor = vectorPercent >= 100 ? 'bg-success' : (vectorPercent >= 50 ? 'bg-purple-500' : (vectorPercent > 0 ? 'bg-purple-400' : 'bg-muted-foreground'));
|
||||
var vectorTextColor = vectorPercent >= 100 ? 'text-success' : (vectorPercent >= 50 ? 'text-purple-500' : (vectorPercent > 0 ? 'text-purple-400' : 'text-muted-foreground'));
|
||||
|
||||
html += '<div class="space-y-1 mt-3">' +
|
||||
'<div class="flex items-center justify-between text-xs">' +
|
||||
'<span class="flex items-center gap-1.5">' +
|
||||
'<i data-lucide="brain" class="w-3.5 h-3.5 text-purple-500"></i> ' +
|
||||
'<span class="font-medium">' + (t('codexlens.vectorIndex') || 'Vector Index') + '</span>' +
|
||||
'</span>' +
|
||||
'<span class="' + vectorTextColor + ' font-medium">' + vectorPercent.toFixed(1) + '%</span>' +
|
||||
'</div>' +
|
||||
'<div class="h-1.5 bg-muted rounded-full overflow-hidden">' +
|
||||
'<div class="h-full ' + vectorColor + ' transition-all duration-300" style="width: ' + vectorPercent + '%"></div>' +
|
||||
'</div>' +
|
||||
'<div class="text-xs text-muted-foreground">' +
|
||||
(result.vector.filesWithEmbeddings || 0) + ' / ' + (result.vector.totalFiles || 0) + ' ' + (t('codexlens.filesWithEmbeddings') || 'files with embeddings') +
|
||||
(result.vector.totalChunks > 0 ? ' (' + result.vector.totalChunks + ' chunks)' : '') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Vector search availability indicator
|
||||
if (vectorPercent >= 50) {
|
||||
html += '<div class="flex items-center gap-1.5 mt-2 pt-2 border-t border-border">' +
|
||||
'<i data-lucide="check-circle-2" class="w-3.5 h-3.5 text-success"></i>' +
|
||||
'<span class="text-xs text-success">' + (t('codexlens.vectorSearchEnabled') || 'Vector search enabled') + '</span>' +
|
||||
'</div>';
|
||||
} else if (vectorPercent > 0) {
|
||||
html += '<div class="flex items-center gap-1.5 mt-2 pt-2 border-t border-border">' +
|
||||
'<i data-lucide="alert-triangle" class="w-3.5 h-3.5 text-warning"></i>' +
|
||||
'<span class="text-xs text-warning">' + (t('codexlens.vectorSearchPartial') || 'Vector search requires ≥50% coverage') + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
} else {
|
||||
// Error from API
|
||||
if (headerFtsEl) headerFtsEl.textContent = '--';
|
||||
if (headerVectorEl) headerVectorEl.textContent = '--';
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="text-xs text-destructive text-center py-2">' +
|
||||
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
|
||||
(result.error || t('common.error') || 'Error loading status') +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
updateWorkspaceStatusUI(result, container, headerFtsEl, headerVectorEl);
|
||||
} catch (err) {
|
||||
console.error('[CodexLens] Failed to load workspace status:', err);
|
||||
if (headerFtsEl) headerFtsEl.textContent = '--';
|
||||
@@ -161,24 +142,151 @@ async function refreshWorkspaceIndexStatus() {
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update workspace status UI with result data
|
||||
* @param {Object} result - API result
|
||||
* @param {HTMLElement} container - Container element
|
||||
* @param {HTMLElement} headerFtsEl - FTS header element
|
||||
* @param {HTMLElement} headerVectorEl - Vector header element
|
||||
*/
|
||||
function updateWorkspaceStatusUI(result, container, headerFtsEl, headerVectorEl) {
|
||||
if (result.success) {
|
||||
var ftsPercent = result.hasIndex ? (result.fts.percent || 0) : 0;
|
||||
var vectorPercent = result.hasIndex ? (result.vector.percent || 0) : 0;
|
||||
|
||||
// Update header badges (always update if elements exist)
|
||||
if (headerFtsEl) {
|
||||
headerFtsEl.textContent = ftsPercent + '%';
|
||||
headerFtsEl.className = 'text-sm font-medium ' +
|
||||
(ftsPercent >= 100 ? 'text-success' : (ftsPercent > 0 ? 'text-blue-500' : 'text-muted-foreground'));
|
||||
}
|
||||
if (headerVectorEl) {
|
||||
headerVectorEl.textContent = vectorPercent.toFixed(1) + '%';
|
||||
headerVectorEl.className = 'text-sm font-medium ' +
|
||||
(vectorPercent >= 100 ? 'text-success' : (vectorPercent >= 50 ? 'text-purple-500' : (vectorPercent > 0 ? 'text-purple-400' : 'text-muted-foreground')));
|
||||
}
|
||||
|
||||
// Update detailed container (if exists)
|
||||
if (container) {
|
||||
var html = '';
|
||||
|
||||
if (!result.hasIndex) {
|
||||
// No index for current workspace
|
||||
html = '<div class="text-center py-3">' +
|
||||
'<div class="text-sm text-muted-foreground mb-2">' +
|
||||
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
|
||||
(t('codexlens.noIndexFound') || 'No index found for current workspace') +
|
||||
'</div>' +
|
||||
'<button onclick="runFtsFullIndex()" class="text-xs text-primary hover:underline">' +
|
||||
(t('codexlens.createIndex') || 'Create Index') +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
} else {
|
||||
// FTS Status
|
||||
var ftsColor = ftsPercent >= 100 ? 'bg-success' : (ftsPercent > 0 ? 'bg-blue-500' : 'bg-muted-foreground');
|
||||
var ftsTextColor = ftsPercent >= 100 ? 'text-success' : (ftsPercent > 0 ? 'text-blue-500' : 'text-muted-foreground');
|
||||
|
||||
html += '<div class="space-y-1">' +
|
||||
'<div class="flex items-center justify-between text-xs">' +
|
||||
'<span class="flex items-center gap-1.5">' +
|
||||
'<i data-lucide="file-text" class="w-3.5 h-3.5 text-blue-500"></i> ' +
|
||||
'<span class="font-medium">' + (t('codexlens.ftsIndex') || 'FTS Index') + '</span>' +
|
||||
'</span>' +
|
||||
'<span class="' + ftsTextColor + ' font-medium">' + ftsPercent + '%</span>' +
|
||||
'</div>' +
|
||||
'<div class="h-1.5 bg-muted rounded-full overflow-hidden">' +
|
||||
'<div class="h-full ' + ftsColor + ' transition-all duration-300" style="width: ' + ftsPercent + '%"></div>' +
|
||||
'</div>' +
|
||||
'<div class="text-xs text-muted-foreground">' +
|
||||
(result.fts.indexedFiles || 0) + ' / ' + (result.fts.totalFiles || 0) + ' ' + (t('codexlens.filesIndexed') || 'files indexed') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Vector Status
|
||||
var vectorColor = vectorPercent >= 100 ? 'bg-success' : (vectorPercent >= 50 ? 'bg-purple-500' : (vectorPercent > 0 ? 'bg-purple-400' : 'bg-muted-foreground'));
|
||||
var vectorTextColor = vectorPercent >= 100 ? 'text-success' : (vectorPercent >= 50 ? 'text-purple-500' : (vectorPercent > 0 ? 'text-purple-400' : 'text-muted-foreground'));
|
||||
|
||||
html += '<div class="space-y-1 mt-3">' +
|
||||
'<div class="flex items-center justify-between text-xs">' +
|
||||
'<span class="flex items-center gap-1.5">' +
|
||||
'<i data-lucide="brain" class="w-3.5 h-3.5 text-purple-500"></i> ' +
|
||||
'<span class="font-medium">' + (t('codexlens.vectorIndex') || 'Vector Index') + '</span>' +
|
||||
'</span>' +
|
||||
'<span class="' + vectorTextColor + ' font-medium">' + vectorPercent.toFixed(1) + '%</span>' +
|
||||
'</div>' +
|
||||
'<div class="h-1.5 bg-muted rounded-full overflow-hidden">' +
|
||||
'<div class="h-full ' + vectorColor + ' transition-all duration-300" style="width: ' + vectorPercent + '%"></div>' +
|
||||
'</div>' +
|
||||
'<div class="text-xs text-muted-foreground">' +
|
||||
(result.vector.filesWithEmbeddings || 0) + ' / ' + (result.vector.totalFiles || 0) + ' ' + (t('codexlens.filesWithEmbeddings') || 'files with embeddings') +
|
||||
(result.vector.totalChunks > 0 ? ' (' + result.vector.totalChunks + ' chunks)' : '') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Vector search availability indicator
|
||||
if (vectorPercent >= 50) {
|
||||
html += '<div class="flex items-center gap-1.5 mt-2 pt-2 border-t border-border">' +
|
||||
'<i data-lucide="check-circle-2" class="w-3.5 h-3.5 text-success"></i>' +
|
||||
'<span class="text-xs text-success">' + (t('codexlens.vectorSearchEnabled') || 'Vector search enabled') + '</span>' +
|
||||
'</div>';
|
||||
} else if (vectorPercent > 0) {
|
||||
html += '<div class="flex items-center gap-1.5 mt-2 pt-2 border-t border-border">' +
|
||||
'<i data-lucide="alert-triangle" class="w-3.5 h-3.5 text-warning"></i>' +
|
||||
'<span class="text-xs text-warning">' + (t('codexlens.vectorSearchPartial') || 'Vector search requires ≥50% coverage') + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
} else {
|
||||
// Error from API
|
||||
if (headerFtsEl) headerFtsEl.textContent = '--';
|
||||
if (headerVectorEl) headerVectorEl.textContent = '--';
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="text-xs text-destructive text-center py-2">' +
|
||||
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
|
||||
(result.error || t('common.error') || 'Error loading status') +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CODEXLENS CONFIGURATION MODAL
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Show CodexLens configuration modal
|
||||
* @param {boolean} forceRefresh - Force refresh, bypass cache
|
||||
*/
|
||||
async function showCodexLensConfigModal() {
|
||||
async function showCodexLensConfigModal(forceRefresh) {
|
||||
try {
|
||||
showRefreshToast(t('codexlens.loadingConfig'), 'info');
|
||||
// Check cache first for config and status
|
||||
var config, status;
|
||||
var usedCache = false;
|
||||
|
||||
// Fetch current config and status in parallel
|
||||
const [configResponse, statusResponse] = await Promise.all([
|
||||
fetch('/api/codexlens/config'),
|
||||
fetch('/api/codexlens/status')
|
||||
]);
|
||||
const config = await configResponse.json();
|
||||
const status = await statusResponse.json();
|
||||
if (!forceRefresh && isCacheValid('config') && isCacheValid('status')) {
|
||||
config = getCachedData('config');
|
||||
status = getCachedData('status');
|
||||
usedCache = true;
|
||||
} else {
|
||||
showRefreshToast(t('codexlens.loadingConfig'), 'info');
|
||||
|
||||
// Fetch current config and status in parallel
|
||||
const [configResponse, statusResponse] = await Promise.all([
|
||||
fetch('/api/codexlens/config'),
|
||||
fetch('/api/codexlens/status')
|
||||
]);
|
||||
config = await configResponse.json();
|
||||
status = await statusResponse.json();
|
||||
|
||||
// Cache the results
|
||||
setCacheData('config', config);
|
||||
setCacheData('status', status);
|
||||
}
|
||||
|
||||
// Update window.cliToolsStatus to ensure isInstalled is correct
|
||||
if (!window.cliToolsStatus) {
|
||||
@@ -6642,3 +6750,13 @@ async function initIgnorePatternsCount() {
|
||||
}
|
||||
}
|
||||
window.initIgnorePatternsCount = initIgnorePatternsCount;
|
||||
|
||||
// ============================================================
|
||||
// CACHE MANAGEMENT - Global Exports
|
||||
// ============================================================
|
||||
window.invalidateCodexLensCache = invalidateCache;
|
||||
window.refreshCodexLensData = async function(forceRefresh) {
|
||||
invalidateCache();
|
||||
await refreshWorkspaceIndexStatus(true);
|
||||
showRefreshToast(t('common.refreshed') || 'Refreshed', 'success');
|
||||
};
|
||||
|
||||
@@ -13,13 +13,13 @@ import * as os from 'os';
|
||||
|
||||
export interface ClaudeCliTool {
|
||||
enabled: boolean;
|
||||
isBuiltin: boolean;
|
||||
command: string;
|
||||
description: string;
|
||||
primaryModel?: string;
|
||||
secondaryModel?: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export type CliToolName = 'gemini' | 'qwen' | 'codex' | 'claude' | 'opencode';
|
||||
|
||||
export interface ClaudeCustomEndpoint {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -37,6 +37,7 @@ export interface ClaudeCacheSettings {
|
||||
export interface ClaudeCliToolsConfig {
|
||||
$schema?: string;
|
||||
version: string;
|
||||
models?: Record<string, string[]>; // PREDEFINED_MODELS
|
||||
tools: Record<string, ClaudeCliTool>;
|
||||
customEndpoints: ClaudeCustomEndpoint[];
|
||||
}
|
||||
@@ -75,43 +76,58 @@ export interface ClaudeCliCombinedConfig extends ClaudeCliToolsConfig {
|
||||
|
||||
// ========== Default Config ==========
|
||||
|
||||
// Predefined models for each tool
|
||||
const PREDEFINED_MODELS: Record<CliToolName, string[]> = {
|
||||
gemini: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash'],
|
||||
qwen: ['coder-model', 'vision-model', 'qwen2.5-coder-32b'],
|
||||
codex: ['gpt-5.2', 'gpt-4.1', 'o4-mini', 'o3'],
|
||||
claude: ['sonnet', 'opus', 'haiku', 'claude-sonnet-4-5-20250929', 'claude-opus-4-5-20251101'],
|
||||
opencode: [
|
||||
'opencode/glm-4.7-free',
|
||||
'opencode/gpt-5-nano',
|
||||
'opencode/grok-code',
|
||||
'opencode/minimax-m2.1-free',
|
||||
'anthropic/claude-sonnet-4-20250514',
|
||||
'anthropic/claude-opus-4-20250514',
|
||||
'openai/gpt-4.1',
|
||||
'openai/o3',
|
||||
'google/gemini-2.5-pro',
|
||||
'google/gemini-2.5-flash'
|
||||
]
|
||||
};
|
||||
|
||||
const DEFAULT_TOOLS_CONFIG: ClaudeCliToolsConfig = {
|
||||
version: '2.0.0',
|
||||
version: '3.0.0',
|
||||
models: { ...PREDEFINED_MODELS },
|
||||
tools: {
|
||||
gemini: {
|
||||
enabled: true,
|
||||
isBuiltin: true,
|
||||
command: 'gemini',
|
||||
description: 'Google AI for code analysis',
|
||||
primaryModel: 'gemini-2.5-pro',
|
||||
secondaryModel: 'gemini-2.5-flash',
|
||||
tags: []
|
||||
},
|
||||
qwen: {
|
||||
enabled: true,
|
||||
isBuiltin: true,
|
||||
command: 'qwen',
|
||||
description: 'Alibaba AI assistant',
|
||||
primaryModel: 'coder-model',
|
||||
secondaryModel: 'coder-model',
|
||||
tags: []
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
isBuiltin: true,
|
||||
command: 'codex',
|
||||
description: 'OpenAI code generation',
|
||||
primaryModel: 'gpt-5.2',
|
||||
secondaryModel: 'gpt-5.2',
|
||||
tags: []
|
||||
},
|
||||
claude: {
|
||||
enabled: true,
|
||||
isBuiltin: true,
|
||||
command: 'claude',
|
||||
description: 'Anthropic AI assistant',
|
||||
primaryModel: 'sonnet',
|
||||
secondaryModel: 'haiku',
|
||||
tags: []
|
||||
},
|
||||
opencode: {
|
||||
enabled: true,
|
||||
isBuiltin: true,
|
||||
command: 'opencode',
|
||||
description: 'OpenCode AI assistant',
|
||||
primaryModel: 'opencode/glm-4.7-free',
|
||||
secondaryModel: 'opencode/glm-4.7-free',
|
||||
tags: []
|
||||
}
|
||||
},
|
||||
@@ -203,19 +219,80 @@ function ensureClaudeDir(projectDir: string): void {
|
||||
// ========== Main Functions ==========
|
||||
|
||||
/**
|
||||
* Ensure tool has tags field (for backward compatibility)
|
||||
* Ensure tool has required fields (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,
|
||||
secondaryModel: tool.secondaryModel,
|
||||
tags: tool.tags ?? []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate config from older versions to v3.0.0
|
||||
*/
|
||||
function migrateConfig(config: any, projectDir: string): ClaudeCliToolsConfig {
|
||||
const version = parseFloat(config.version || '1.0');
|
||||
|
||||
// Already v3.x, no migration needed
|
||||
if (version >= 3.0) {
|
||||
return config as ClaudeCliToolsConfig;
|
||||
}
|
||||
|
||||
console.log(`[claude-cli-tools] Migrating config from v${config.version || '1.0'} to v3.0.0`);
|
||||
|
||||
// Try to load legacy cli-config.json for model data
|
||||
let legacyCliConfig: any = null;
|
||||
try {
|
||||
const { StoragePaths } = require('../config/storage-paths.js');
|
||||
const legacyPath = StoragePaths.project(projectDir).cliConfig;
|
||||
const fs = require('fs');
|
||||
if (fs.existsSync(legacyPath)) {
|
||||
legacyCliConfig = JSON.parse(fs.readFileSync(legacyPath, 'utf-8'));
|
||||
console.log(`[claude-cli-tools] Found legacy cli-config.json, merging model data`);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors loading legacy config
|
||||
}
|
||||
|
||||
const migratedTools: Record<string, ClaudeCliTool> = {};
|
||||
|
||||
for (const [key, tool] of Object.entries(config.tools || {})) {
|
||||
const t = tool as any;
|
||||
const legacyTool = legacyCliConfig?.tools?.[key];
|
||||
|
||||
migratedTools[key] = {
|
||||
enabled: t.enabled ?? legacyTool?.enabled ?? true,
|
||||
primaryModel: t.primaryModel ?? legacyTool?.primaryModel ?? DEFAULT_TOOLS_CONFIG.tools[key]?.primaryModel,
|
||||
secondaryModel: t.secondaryModel ?? legacyTool?.secondaryModel ?? DEFAULT_TOOLS_CONFIG.tools[key]?.secondaryModel,
|
||||
tags: t.tags ?? legacyTool?.tags ?? []
|
||||
};
|
||||
}
|
||||
|
||||
// Add any missing default tools
|
||||
for (const [key, defaultTool] of Object.entries(DEFAULT_TOOLS_CONFIG.tools)) {
|
||||
if (!migratedTools[key]) {
|
||||
const legacyTool = legacyCliConfig?.tools?.[key];
|
||||
migratedTools[key] = {
|
||||
enabled: legacyTool?.enabled ?? defaultTool.enabled,
|
||||
primaryModel: legacyTool?.primaryModel ?? defaultTool.primaryModel,
|
||||
secondaryModel: legacyTool?.secondaryModel ?? defaultTool.secondaryModel,
|
||||
tags: legacyTool?.tags ?? defaultTool.tags
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: '3.0.0',
|
||||
models: { ...PREDEFINED_MODELS },
|
||||
tools: migratedTools,
|
||||
customEndpoints: config.customEndpoints || [],
|
||||
$schema: config.$schema
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure CLI tools configuration file exists
|
||||
* Creates default config if missing (auto-rebuild feature)
|
||||
@@ -270,6 +347,8 @@ export function ensureClaudeCliTools(projectDir: string, createInProject: boolea
|
||||
* 1. Project: {projectDir}/.claude/cli-tools.json
|
||||
* 2. Global: ~/.claude/cli-tools.json
|
||||
* 3. Default config
|
||||
*
|
||||
* Automatically migrates older config versions to v3.0.0
|
||||
*/
|
||||
export function loadClaudeCliTools(projectDir: string): ClaudeCliToolsConfig & { _source?: string } {
|
||||
const resolved = resolveConfigPath(projectDir);
|
||||
@@ -282,26 +361,41 @@ export function loadClaudeCliTools(projectDir: string): ClaudeCliToolsConfig & {
|
||||
const content = fs.readFileSync(resolved.path, 'utf-8');
|
||||
const parsed = JSON.parse(content) as Partial<ClaudeCliCombinedConfig>;
|
||||
|
||||
// Merge tools with defaults and ensure tags exist
|
||||
// Migrate older versions to v3.0.0
|
||||
const migrated = migrateConfig(parsed, projectDir);
|
||||
const needsSave = migrated.version !== parsed.version;
|
||||
|
||||
// Merge tools with defaults and ensure required fields exist
|
||||
const mergedTools: Record<string, ClaudeCliTool> = {};
|
||||
for (const [key, tool] of Object.entries({ ...DEFAULT_TOOLS_CONFIG.tools, ...(parsed.tools || {}) })) {
|
||||
for (const [key, tool] of Object.entries({ ...DEFAULT_TOOLS_CONFIG.tools, ...(migrated.tools || {}) })) {
|
||||
mergedTools[key] = ensureToolTags(tool);
|
||||
}
|
||||
|
||||
// Ensure customEndpoints have tags
|
||||
const mergedEndpoints = (parsed.customEndpoints || []).map(ep => ({
|
||||
const mergedEndpoints = (migrated.customEndpoints || []).map(ep => ({
|
||||
...ep,
|
||||
tags: ep.tags ?? []
|
||||
}));
|
||||
|
||||
const config: ClaudeCliToolsConfig & { _source?: string } = {
|
||||
version: parsed.version || DEFAULT_TOOLS_CONFIG.version,
|
||||
version: migrated.version || DEFAULT_TOOLS_CONFIG.version,
|
||||
models: migrated.models || DEFAULT_TOOLS_CONFIG.models,
|
||||
tools: mergedTools,
|
||||
customEndpoints: mergedEndpoints,
|
||||
$schema: parsed.$schema,
|
||||
$schema: migrated.$schema,
|
||||
_source: resolved.source
|
||||
};
|
||||
|
||||
// Save migrated config if version changed
|
||||
if (needsSave) {
|
||||
try {
|
||||
saveClaudeCliTools(projectDir, config);
|
||||
console.log(`[claude-cli-tools] Saved migrated config to: ${resolved.path}`);
|
||||
} catch (err) {
|
||||
console.warn('[claude-cli-tools] Failed to save migrated config:', err);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[claude-cli-tools] Loaded tools config from ${resolved.source}: ${resolved.path}`);
|
||||
return config;
|
||||
} catch (err) {
|
||||
@@ -578,3 +672,122 @@ export function getContextToolsPath(provider: 'codexlens' | 'ace' | 'none'): str
|
||||
return 'context-tools.md';
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Model Configuration Functions ==========
|
||||
|
||||
/**
|
||||
* Get predefined models for a specific tool
|
||||
*/
|
||||
export function getPredefinedModels(tool: string): string[] {
|
||||
const toolName = tool as CliToolName;
|
||||
return PREDEFINED_MODELS[toolName] ? [...PREDEFINED_MODELS[toolName]] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all predefined models
|
||||
*/
|
||||
export function getAllPredefinedModels(): Record<string, string[]> {
|
||||
return { ...PREDEFINED_MODELS };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool configuration (compatible with cli-config-manager interface)
|
||||
*/
|
||||
export function getToolConfig(projectDir: string, tool: string): {
|
||||
enabled: boolean;
|
||||
primaryModel: string;
|
||||
secondaryModel: string;
|
||||
tags?: string[];
|
||||
} {
|
||||
const config = loadClaudeCliTools(projectDir);
|
||||
const toolConfig = config.tools[tool];
|
||||
|
||||
if (!toolConfig) {
|
||||
const defaultTool = DEFAULT_TOOLS_CONFIG.tools[tool];
|
||||
return {
|
||||
enabled: defaultTool?.enabled ?? true,
|
||||
primaryModel: defaultTool?.primaryModel ?? '',
|
||||
secondaryModel: defaultTool?.secondaryModel ?? '',
|
||||
tags: defaultTool?.tags ?? []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: toolConfig.enabled,
|
||||
primaryModel: toolConfig.primaryModel ?? '',
|
||||
secondaryModel: toolConfig.secondaryModel ?? '',
|
||||
tags: toolConfig.tags
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tool configuration
|
||||
*/
|
||||
export function updateToolConfig(
|
||||
projectDir: string,
|
||||
tool: string,
|
||||
updates: Partial<{
|
||||
enabled: boolean;
|
||||
primaryModel: string;
|
||||
secondaryModel: string;
|
||||
tags: string[];
|
||||
}>
|
||||
): ClaudeCliToolsConfig {
|
||||
const config = loadClaudeCliTools(projectDir);
|
||||
|
||||
if (config.tools[tool]) {
|
||||
if (updates.enabled !== undefined) {
|
||||
config.tools[tool].enabled = updates.enabled;
|
||||
}
|
||||
if (updates.primaryModel !== undefined) {
|
||||
config.tools[tool].primaryModel = updates.primaryModel;
|
||||
}
|
||||
if (updates.secondaryModel !== undefined) {
|
||||
config.tools[tool].secondaryModel = updates.secondaryModel;
|
||||
}
|
||||
if (updates.tags !== undefined) {
|
||||
config.tools[tool].tags = updates.tags;
|
||||
}
|
||||
saveClaudeCliTools(projectDir, config);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get primary model for a tool
|
||||
*/
|
||||
export function getPrimaryModel(projectDir: string, tool: string): string {
|
||||
const toolConfig = getToolConfig(projectDir, tool);
|
||||
return toolConfig.primaryModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get secondary model for a tool
|
||||
*/
|
||||
export function getSecondaryModel(projectDir: string, tool: string): string {
|
||||
const toolConfig = getToolConfig(projectDir, tool);
|
||||
return toolConfig.secondaryModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool is enabled
|
||||
*/
|
||||
export function isToolEnabled(projectDir: string, tool: string): boolean {
|
||||
const toolConfig = getToolConfig(projectDir, tool);
|
||||
return toolConfig.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full config response for API (includes predefined models)
|
||||
*/
|
||||
export function getFullConfigResponse(projectDir: string): {
|
||||
config: ClaudeCliToolsConfig;
|
||||
predefinedModels: Record<string, string[]>;
|
||||
} {
|
||||
const config = loadClaudeCliTools(projectDir);
|
||||
return {
|
||||
config,
|
||||
predefinedModels: { ...PREDEFINED_MODELS }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
/**
|
||||
* CLI Configuration Manager
|
||||
* Handles loading, saving, and managing CLI tool configurations
|
||||
* Stores config in centralized storage (~/.ccw/projects/{id}/config/)
|
||||
* CLI Configuration Manager (Deprecated - Redirects to claude-cli-tools.ts)
|
||||
*
|
||||
* This module is maintained for backward compatibility.
|
||||
* All configuration is now managed by claude-cli-tools.ts using cli-tools.json
|
||||
*
|
||||
* @deprecated Use claude-cli-tools.ts directly
|
||||
*/
|
||||
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';
|
||||
import {
|
||||
loadClaudeCliTools,
|
||||
saveClaudeCliTools,
|
||||
getToolConfig as getToolConfigFromClaude,
|
||||
updateToolConfig as updateToolConfigFromClaude,
|
||||
getPredefinedModels as getPredefinedModelsFromClaude,
|
||||
getAllPredefinedModels,
|
||||
getPrimaryModel as getPrimaryModelFromClaude,
|
||||
getSecondaryModel as getSecondaryModelFromClaude,
|
||||
isToolEnabled as isToolEnabledFromClaude,
|
||||
getFullConfigResponse as getFullConfigResponseFromClaude,
|
||||
type ClaudeCliTool,
|
||||
type ClaudeCliToolsConfig,
|
||||
type CliToolName
|
||||
} from './claude-cli-tools.js';
|
||||
|
||||
// ========== Types ==========
|
||||
// ========== Re-exported Types ==========
|
||||
|
||||
export interface CliToolConfig {
|
||||
enabled: boolean;
|
||||
primaryModel: string; // For CLI endpoint calls (ccw cli -p)
|
||||
secondaryModel: string; // For internal calls (llm_enhancer, generate_module_docs)
|
||||
tags?: string[]; // User-defined tags/labels for the tool
|
||||
primaryModel: string;
|
||||
secondaryModel: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface CliConfig {
|
||||
@@ -22,234 +36,94 @@ export interface CliConfig {
|
||||
tools: Record<string, CliToolConfig>;
|
||||
}
|
||||
|
||||
export type CliToolName = 'gemini' | 'qwen' | 'codex' | 'claude' | 'opencode';
|
||||
export type { CliToolName };
|
||||
|
||||
// ========== Constants ==========
|
||||
// ========== Re-exported Constants ==========
|
||||
|
||||
export const PREDEFINED_MODELS: Record<CliToolName, string[]> = {
|
||||
gemini: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash'],
|
||||
qwen: ['coder-model', 'vision-model', 'qwen2.5-coder-32b'],
|
||||
codex: ['gpt-5.2', 'gpt-4.1', 'o4-mini', 'o3'],
|
||||
claude: ['sonnet', 'opus', 'haiku', 'claude-sonnet-4-5-20250929', 'claude-opus-4-5-20251101'],
|
||||
opencode: [
|
||||
'opencode/glm-4.7-free',
|
||||
'opencode/gpt-5-nano',
|
||||
'opencode/grok-code',
|
||||
'opencode/minimax-m2.1-free',
|
||||
'anthropic/claude-sonnet-4-20250514',
|
||||
'anthropic/claude-opus-4-20250514',
|
||||
'openai/gpt-4.1',
|
||||
'openai/o3',
|
||||
'google/gemini-2.5-pro',
|
||||
'google/gemini-2.5-flash'
|
||||
]
|
||||
};
|
||||
/**
|
||||
* @deprecated Use getPredefinedModels() or getAllPredefinedModels() instead
|
||||
*/
|
||||
export const PREDEFINED_MODELS = getAllPredefinedModels();
|
||||
|
||||
/**
|
||||
* @deprecated Default config is now managed in claude-cli-tools.ts
|
||||
*/
|
||||
export const DEFAULT_CONFIG: CliConfig = {
|
||||
version: 1,
|
||||
tools: {
|
||||
gemini: {
|
||||
enabled: true,
|
||||
primaryModel: 'gemini-2.5-pro',
|
||||
secondaryModel: 'gemini-2.5-flash'
|
||||
},
|
||||
qwen: {
|
||||
enabled: true,
|
||||
primaryModel: 'coder-model',
|
||||
secondaryModel: 'coder-model'
|
||||
},
|
||||
codex: {
|
||||
enabled: true,
|
||||
primaryModel: 'gpt-5.2',
|
||||
secondaryModel: 'gpt-5.2'
|
||||
},
|
||||
claude: {
|
||||
enabled: true,
|
||||
primaryModel: 'sonnet',
|
||||
secondaryModel: 'haiku'
|
||||
},
|
||||
opencode: {
|
||||
enabled: true,
|
||||
primaryModel: 'opencode/glm-4.7-free', // Free model as default
|
||||
secondaryModel: 'opencode/glm-4.7-free'
|
||||
}
|
||||
gemini: { enabled: true, primaryModel: 'gemini-2.5-pro', secondaryModel: 'gemini-2.5-flash' },
|
||||
qwen: { enabled: true, primaryModel: 'coder-model', secondaryModel: 'coder-model' },
|
||||
codex: { enabled: true, primaryModel: 'gpt-5.2', secondaryModel: 'gpt-5.2' },
|
||||
claude: { enabled: true, primaryModel: 'sonnet', secondaryModel: 'haiku' },
|
||||
opencode: { enabled: true, primaryModel: 'opencode/glm-4.7-free', secondaryModel: 'opencode/glm-4.7-free' }
|
||||
}
|
||||
};
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
// ========== Re-exported Functions ==========
|
||||
|
||||
function getConfigPath(baseDir: string): string {
|
||||
return StoragePaths.project(baseDir).cliConfig;
|
||||
}
|
||||
/**
|
||||
* Load CLI configuration
|
||||
* @deprecated Use loadClaudeCliTools() instead
|
||||
*/
|
||||
export function loadCliConfig(baseDir: string): CliConfig {
|
||||
const config = loadClaudeCliTools(baseDir);
|
||||
|
||||
function ensureConfigDirForProject(baseDir: string): void {
|
||||
const configDir = StoragePaths.project(baseDir).config;
|
||||
ensureStorageDir(configDir);
|
||||
}
|
||||
|
||||
function isValidToolName(tool: string): tool is CliToolName {
|
||||
return ['gemini', 'qwen', 'codex', 'claude', 'opencode'].includes(tool);
|
||||
}
|
||||
|
||||
function validateConfig(config: unknown): config is CliConfig {
|
||||
if (!config || typeof config !== 'object') return false;
|
||||
const c = config as Record<string, unknown>;
|
||||
|
||||
if (typeof c.version !== 'number') return false;
|
||||
if (!c.tools || typeof c.tools !== 'object') return false;
|
||||
|
||||
const tools = c.tools as Record<string, unknown>;
|
||||
for (const toolName of ['gemini', 'qwen', 'codex', 'claude', 'opencode']) {
|
||||
const tool = tools[toolName];
|
||||
if (!tool || typeof tool !== 'object') return false;
|
||||
|
||||
const t = tool as Record<string, unknown>;
|
||||
if (typeof t.enabled !== 'boolean') return false;
|
||||
if (typeof t.primaryModel !== 'string') return false;
|
||||
if (typeof t.secondaryModel !== 'string') return false;
|
||||
// Convert to legacy format
|
||||
const tools: Record<string, CliToolConfig> = {};
|
||||
for (const [key, tool] of Object.entries(config.tools)) {
|
||||
tools[key] = {
|
||||
enabled: tool.enabled,
|
||||
primaryModel: tool.primaryModel ?? '',
|
||||
secondaryModel: tool.secondaryModel ?? '',
|
||||
tags: tool.tags
|
||||
};
|
||||
}
|
||||
|
||||
return true;
|
||||
return {
|
||||
version: parseFloat(config.version) || 1,
|
||||
tools
|
||||
};
|
||||
}
|
||||
|
||||
function mergeWithDefaults(config: Partial<CliConfig>): CliConfig {
|
||||
const result: CliConfig = {
|
||||
version: config.version ?? DEFAULT_CONFIG.version,
|
||||
tools: { ...DEFAULT_CONFIG.tools }
|
||||
};
|
||||
/**
|
||||
* Save CLI configuration
|
||||
* @deprecated Use saveClaudeCliTools() instead
|
||||
*/
|
||||
export function saveCliConfig(baseDir: string, config: CliConfig): void {
|
||||
const currentConfig = loadClaudeCliTools(baseDir);
|
||||
|
||||
if (config.tools) {
|
||||
for (const toolName of Object.keys(config.tools)) {
|
||||
if (isValidToolName(toolName) && config.tools[toolName]) {
|
||||
result.tools[toolName] = {
|
||||
...DEFAULT_CONFIG.tools[toolName],
|
||||
...config.tools[toolName]
|
||||
};
|
||||
// Update tools from legacy format
|
||||
for (const [key, tool] of Object.entries(config.tools)) {
|
||||
if (currentConfig.tools[key]) {
|
||||
currentConfig.tools[key].enabled = tool.enabled;
|
||||
currentConfig.tools[key].primaryModel = tool.primaryModel;
|
||||
currentConfig.tools[key].secondaryModel = tool.secondaryModel;
|
||||
if (tool.tags) {
|
||||
currentConfig.tools[key].tags = tool.tags;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ========== Main Functions ==========
|
||||
|
||||
/**
|
||||
* Load CLI configuration from .workflow/cli-config.json
|
||||
* Returns default config if file doesn't exist or is invalid
|
||||
*/
|
||||
export function loadCliConfig(baseDir: string): CliConfig {
|
||||
const configPath = getConfigPath(baseDir);
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(configPath)) {
|
||||
return { ...DEFAULT_CONFIG };
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
const parsed = JSON.parse(content);
|
||||
|
||||
if (validateConfig(parsed)) {
|
||||
return mergeWithDefaults(parsed);
|
||||
}
|
||||
|
||||
// Invalid config, return defaults
|
||||
console.warn('[cli-config] Invalid config file, using defaults');
|
||||
return { ...DEFAULT_CONFIG };
|
||||
} catch (err) {
|
||||
console.error('[cli-config] Error loading config:', err);
|
||||
return { ...DEFAULT_CONFIG };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save CLI configuration to .workflow/cli-config.json
|
||||
*/
|
||||
export function saveCliConfig(baseDir: string, config: CliConfig): void {
|
||||
ensureConfigDirForProject(baseDir);
|
||||
const configPath = getConfigPath(baseDir);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
} catch (err) {
|
||||
console.error('[cli-config] Error saving config:', err);
|
||||
throw new Error(`Failed to save CLI config: ${err}`);
|
||||
}
|
||||
saveClaudeCliTools(baseDir, currentConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration for a specific tool
|
||||
*/
|
||||
export function getToolConfig(baseDir: string, tool: string): CliToolConfig {
|
||||
if (!isValidToolName(tool)) {
|
||||
throw new Error(`Invalid tool name: ${tool}`);
|
||||
}
|
||||
|
||||
const config = loadCliConfig(baseDir);
|
||||
return config.tools[tool] || DEFAULT_CONFIG.tools[tool];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and sanitize tags array
|
||||
* @param tags - Raw tags array from user input
|
||||
* @returns Sanitized tags array
|
||||
*/
|
||||
function validateTags(tags: string[] | undefined): string[] | undefined {
|
||||
if (!tags || !Array.isArray(tags)) return undefined;
|
||||
|
||||
const MAX_TAGS = 10;
|
||||
const MAX_TAG_LENGTH = 30;
|
||||
|
||||
return tags
|
||||
.filter(tag => typeof tag === 'string')
|
||||
.map(tag => tag.trim())
|
||||
.filter(tag => tag.length > 0 && tag.length <= MAX_TAG_LENGTH)
|
||||
.slice(0, MAX_TAGS);
|
||||
return getToolConfigFromClaude(baseDir, tool);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update configuration for a specific tool
|
||||
* Returns the updated tool config
|
||||
*/
|
||||
export function updateToolConfig(
|
||||
baseDir: string,
|
||||
tool: string,
|
||||
updates: Partial<CliToolConfig>
|
||||
): CliToolConfig {
|
||||
if (!isValidToolName(tool)) {
|
||||
throw new Error(`Invalid tool name: ${tool}`);
|
||||
}
|
||||
|
||||
const config = loadCliConfig(baseDir);
|
||||
const currentToolConfig = config.tools[tool] || DEFAULT_CONFIG.tools[tool];
|
||||
|
||||
// Apply updates
|
||||
const updatedToolConfig: CliToolConfig = {
|
||||
enabled: updates.enabled !== undefined ? updates.enabled : currentToolConfig.enabled,
|
||||
primaryModel: updates.primaryModel || currentToolConfig.primaryModel,
|
||||
secondaryModel: updates.secondaryModel || currentToolConfig.secondaryModel,
|
||||
tags: updates.tags !== undefined ? validateTags(updates.tags) : currentToolConfig.tags
|
||||
};
|
||||
|
||||
// Save updated config
|
||||
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;
|
||||
updateToolConfigFromClaude(baseDir, tool, updates);
|
||||
return getToolConfig(baseDir, tool);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -270,73 +144,55 @@ export function disableTool(baseDir: string, tool: string): CliToolConfig {
|
||||
* Check if a tool is enabled
|
||||
*/
|
||||
export function isToolEnabled(baseDir: string, tool: string): boolean {
|
||||
try {
|
||||
const config = getToolConfig(baseDir, tool);
|
||||
return config.enabled;
|
||||
} catch {
|
||||
return true; // Default to enabled if error
|
||||
}
|
||||
return isToolEnabledFromClaude(baseDir, tool);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get primary model for a tool
|
||||
*/
|
||||
export function getPrimaryModel(baseDir: string, tool: string): string {
|
||||
try {
|
||||
const config = getToolConfig(baseDir, tool);
|
||||
return config.primaryModel;
|
||||
} catch {
|
||||
return isValidToolName(tool) ? DEFAULT_CONFIG.tools[tool].primaryModel : 'gemini-2.5-pro';
|
||||
}
|
||||
return getPrimaryModelFromClaude(baseDir, tool);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get secondary model for a tool (used for internal calls)
|
||||
* Get secondary model for a tool
|
||||
*/
|
||||
export function getSecondaryModel(baseDir: string, tool: string): string {
|
||||
try {
|
||||
const config = getToolConfig(baseDir, tool);
|
||||
return config.secondaryModel;
|
||||
} catch {
|
||||
return isValidToolName(tool) ? DEFAULT_CONFIG.tools[tool].secondaryModel : 'gemini-2.5-flash';
|
||||
}
|
||||
return getSecondaryModelFromClaude(baseDir, tool);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all predefined models for a tool
|
||||
*/
|
||||
export function getPredefinedModels(tool: string): string[] {
|
||||
if (!isValidToolName(tool)) {
|
||||
return [];
|
||||
}
|
||||
return [...PREDEFINED_MODELS[tool]];
|
||||
return getPredefinedModelsFromClaude(tool);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full config response for API (includes predefined models and tags from cli-tools.json)
|
||||
* Get full config response for API
|
||||
*/
|
||||
export function getFullConfigResponse(baseDir: string): {
|
||||
config: CliConfig;
|
||||
predefinedModels: Record<string, string[]>;
|
||||
} {
|
||||
const config = loadCliConfig(baseDir);
|
||||
const response = getFullConfigResponseFromClaude(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);
|
||||
// Convert to legacy format
|
||||
const tools: Record<string, CliToolConfig> = {};
|
||||
for (const [key, tool] of Object.entries(response.config.tools)) {
|
||||
tools[key] = {
|
||||
enabled: tool.enabled,
|
||||
primaryModel: tool.primaryModel ?? '',
|
||||
secondaryModel: tool.secondaryModel ?? '',
|
||||
tags: tool.tags
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
predefinedModels: { ...PREDEFINED_MODELS }
|
||||
config: {
|
||||
version: parseFloat(response.config.version) || 1,
|
||||
tools
|
||||
},
|
||||
predefinedModels: response.predefinedModels
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { executeInitWithProgress } from './smart-search.js';
|
||||
import * as readFileMod from './read-file.js';
|
||||
import * as coreMemoryMod from './core-memory.js';
|
||||
import * as contextCacheMod from './context-cache.js';
|
||||
import * as skillContextLoaderMod from './skill-context-loader.js';
|
||||
import type { ProgressInfo } from './codex-lens.js';
|
||||
|
||||
// Import legacy JS tools
|
||||
@@ -359,6 +360,7 @@ registerTool(toLegacyTool(smartSearchMod));
|
||||
registerTool(toLegacyTool(readFileMod));
|
||||
registerTool(toLegacyTool(coreMemoryMod));
|
||||
registerTool(toLegacyTool(contextCacheMod));
|
||||
registerTool(toLegacyTool(skillContextLoaderMod));
|
||||
|
||||
// Register legacy JS tools
|
||||
registerTool(uiGeneratePreviewTool);
|
||||
|
||||
213
ccw/src/tools/skill-context-loader.ts
Normal file
213
ccw/src/tools/skill-context-loader.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Skill Context Loader Tool
|
||||
* Loads SKILL context based on keyword matching in user prompt
|
||||
* Used by UserPromptSubmit hooks to inject skill context
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { ToolSchema, ToolResult } from '../types/tool.js';
|
||||
import { readFileSync, existsSync, readdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
// Input schema for keyword mode config
|
||||
const SkillConfigSchema = z.object({
|
||||
skill: z.string(),
|
||||
keywords: z.array(z.string())
|
||||
});
|
||||
|
||||
// Main params schema
|
||||
const ParamsSchema = z.object({
|
||||
// Auto mode flag
|
||||
mode: z.literal('auto').optional(),
|
||||
// User prompt to match against
|
||||
prompt: z.string(),
|
||||
// Keyword mode configs (only for keyword mode)
|
||||
configs: z.array(SkillConfigSchema).optional()
|
||||
});
|
||||
|
||||
type Params = z.infer<typeof ParamsSchema>;
|
||||
|
||||
/**
|
||||
* Get all available skill names from project and user directories
|
||||
*/
|
||||
function getAvailableSkills(): Array<{ name: string; folderName: string; location: 'project' | 'user' }> {
|
||||
const skills: Array<{ name: string; folderName: string; location: 'project' | 'user' }> = [];
|
||||
|
||||
// Project skills
|
||||
const projectSkillsDir = join(process.cwd(), '.claude', 'skills');
|
||||
if (existsSync(projectSkillsDir)) {
|
||||
try {
|
||||
const entries = readdirSync(projectSkillsDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const skillMdPath = join(projectSkillsDir, entry.name, 'SKILL.md');
|
||||
if (existsSync(skillMdPath)) {
|
||||
const name = parseSkillName(skillMdPath) || entry.name;
|
||||
skills.push({ name, folderName: entry.name, location: 'project' });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
// User skills
|
||||
const userSkillsDir = join(homedir(), '.claude', 'skills');
|
||||
if (existsSync(userSkillsDir)) {
|
||||
try {
|
||||
const entries = readdirSync(userSkillsDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const skillMdPath = join(userSkillsDir, entry.name, 'SKILL.md');
|
||||
if (existsSync(skillMdPath)) {
|
||||
const name = parseSkillName(skillMdPath) || entry.name;
|
||||
// Skip if already added from project (project takes priority)
|
||||
if (!skills.some(s => s.folderName === entry.name)) {
|
||||
skills.push({ name, folderName: entry.name, location: 'user' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
return skills;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse skill name from SKILL.md frontmatter
|
||||
*/
|
||||
function parseSkillName(skillMdPath: string): string | null {
|
||||
try {
|
||||
const content = readFileSync(skillMdPath, 'utf8');
|
||||
if (content.startsWith('---')) {
|
||||
const endIndex = content.indexOf('---', 3);
|
||||
if (endIndex > 0) {
|
||||
const frontmatter = content.substring(3, endIndex);
|
||||
const nameMatch = frontmatter.match(/^name:\s*["']?([^"'\n]+)["']?/m);
|
||||
if (nameMatch) {
|
||||
return nameMatch[1].trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match prompt against keywords (case-insensitive)
|
||||
*/
|
||||
function matchKeywords(prompt: string, keywords: string[]): string | null {
|
||||
const lowerPrompt = prompt.toLowerCase();
|
||||
for (const keyword of keywords) {
|
||||
if (keyword && lowerPrompt.includes(keyword.toLowerCase())) {
|
||||
return keyword;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format skill invocation instruction for hook output
|
||||
* Returns a prompt to invoke the skill, not the full content
|
||||
*/
|
||||
function formatSkillInvocation(skillName: string, matchedKeyword?: string): string {
|
||||
return `Use /${skillName} skill to handle this request.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool schema definition
|
||||
*/
|
||||
export const schema: ToolSchema = {
|
||||
name: 'skill_context_loader',
|
||||
description: 'Match keywords in user prompt and return skill invocation instruction. Returns "Use /skill-name skill" when keywords match.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
mode: {
|
||||
type: 'string',
|
||||
enum: ['auto'],
|
||||
description: 'Auto mode: detect skill name in prompt automatically'
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
description: 'User prompt to match against keywords'
|
||||
},
|
||||
configs: {
|
||||
type: 'array',
|
||||
description: 'Keyword mode: array of skill configs with keywords',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
skill: { type: 'string', description: 'Skill folder name to load' },
|
||||
keywords: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Keywords to match in prompt'
|
||||
}
|
||||
},
|
||||
required: ['skill', 'keywords']
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['prompt']
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tool handler
|
||||
*/
|
||||
export async function handler(params: Record<string, unknown>): Promise<ToolResult<string>> {
|
||||
try {
|
||||
const parsed = ParamsSchema.parse(params);
|
||||
const { mode, prompt, configs } = parsed;
|
||||
|
||||
// Auto mode: detect skill name in prompt
|
||||
if (mode === 'auto') {
|
||||
const skills = getAvailableSkills();
|
||||
const lowerPrompt = prompt.toLowerCase();
|
||||
|
||||
for (const skill of skills) {
|
||||
// Check if prompt contains skill name or folder name
|
||||
if (lowerPrompt.includes(skill.name.toLowerCase()) ||
|
||||
lowerPrompt.includes(skill.folderName.toLowerCase())) {
|
||||
return {
|
||||
success: true,
|
||||
result: formatSkillInvocation(skill.folderName, skill.name)
|
||||
};
|
||||
}
|
||||
}
|
||||
// No match - return empty (silent)
|
||||
return { success: true, result: '' };
|
||||
}
|
||||
|
||||
// Keyword mode: match against configured keywords
|
||||
if (configs && configs.length > 0) {
|
||||
for (const config of configs) {
|
||||
const matchedKeyword = matchKeywords(prompt, config.keywords);
|
||||
if (matchedKeyword) {
|
||||
return {
|
||||
success: true,
|
||||
result: formatSkillInvocation(config.skill, matchedKeyword)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No match - return empty (silent)
|
||||
return { success: true, result: '' };
|
||||
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
success: false,
|
||||
error: `skill_context_loader error: ${message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user