mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-25 19:48:33 +08:00
feat: add agent definitions API for managing Codex and Claude agent configurations
This commit is contained in:
@@ -4,7 +4,7 @@ model = "gpt-5.4"
|
|||||||
model_reasoning_effort = "high"
|
model_reasoning_effort = "high"
|
||||||
sandbox_mode = "workspace-write"
|
sandbox_mode = "workspace-write"
|
||||||
|
|
||||||
developer_instructions = """
|
developer_instructions = '''
|
||||||
|
|
||||||
You are an intelligent CLI execution specialist that autonomously orchestrates context discovery and optimal tool execution.
|
You are an intelligent CLI execution specialist that autonomously orchestrates context discovery and optimal tool execution.
|
||||||
|
|
||||||
@@ -331,4 +331,4 @@ Codex unavailable → Gemini/Qwen write mode
|
|||||||
- `claude-module-unified.txt` - Universal module/file documentation
|
- `claude-module-unified.txt` - Universal module/file documentation
|
||||||
|
|
||||||
---
|
---
|
||||||
"""
|
'''
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ model = "gpt-5.4"
|
|||||||
model_reasoning_effort = "high"
|
model_reasoning_effort = "high"
|
||||||
sandbox_mode = "read-only"
|
sandbox_mode = "read-only"
|
||||||
|
|
||||||
developer_instructions = """
|
developer_instructions = '''
|
||||||
|
|
||||||
You are a specialized CLI exploration agent that autonomously analyzes codebases and generates structured outputs.
|
You are a specialized CLI exploration agent that autonomously analyzes codebases and generates structured outputs.
|
||||||
|
|
||||||
@@ -229,4 +229,4 @@ Brief summary:
|
|||||||
**Consumption Pattern**:
|
**Consumption Pattern**:
|
||||||
- Plan phase: Fully consumes `exploration-notes.md`
|
- Plan phase: Fully consumes `exploration-notes.md`
|
||||||
- Execute phase: Consumes `exploration-notes-refined.md`, reduced noise, improved efficiency
|
- Execute phase: Consumes `exploration-notes-refined.md`, reduced noise, improved efficiency
|
||||||
"""
|
'''
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ model = "gpt-5.4"
|
|||||||
model_reasoning_effort = "high"
|
model_reasoning_effort = "high"
|
||||||
sandbox_mode = "workspace-write"
|
sandbox_mode = "workspace-write"
|
||||||
|
|
||||||
developer_instructions = """
|
developer_instructions = '''
|
||||||
|
|
||||||
You are a generic planning agent that generates structured plan JSON for lite workflows. Output format is determined by the schema reference provided in the prompt. You execute CLI planning tools (Gemini/Qwen), parse results, and generate planObject conforming to the specified schema.
|
You are a generic planning agent that generates structured plan JSON for lite workflows. Output format is determined by the schema reference provided in the prompt. You execute CLI planning tools (Gemini/Qwen), parse results, and generate planObject conforming to the specified schema.
|
||||||
|
|
||||||
@@ -898,4 +898,4 @@ After Phase 4 planObject generation:
|
|||||||
5. **Return** → Plan with `_metadata.quality_check` containing execution result
|
5. **Return** → Plan with `_metadata.quality_check` containing execution result
|
||||||
|
|
||||||
**CLI Fallback**: Gemini → Qwen → Skip with warning (if both fail)
|
**CLI Fallback**: Gemini → Qwen → Skip with warning (if both fail)
|
||||||
"""
|
'''
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ model = "gpt-5.4"
|
|||||||
model_reasoning_effort = "high"
|
model_reasoning_effort = "high"
|
||||||
sandbox_mode = "workspace-write"
|
sandbox_mode = "workspace-write"
|
||||||
|
|
||||||
developer_instructions = """
|
developer_instructions = '''
|
||||||
|
|
||||||
You are a specialized execution agent that bridges CLI analysis tools with task generation. You execute Gemini/Qwen CLI commands for failure diagnosis, parse structured results, and dynamically generate task JSON files for downstream execution.
|
You are a specialized execution agent that bridges CLI analysis tools with task generation. You execute Gemini/Qwen CLI commands for failure diagnosis, parse structured results, and dynamically generate task JSON files for downstream execution.
|
||||||
|
|
||||||
@@ -550,4 +550,4 @@ See: `.process/iteration-{iteration}-cli-output.txt`
|
|||||||
estimated_complexity: "medium"
|
estimated_complexity: "medium"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
"""
|
'''
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ model = "gpt-5.4"
|
|||||||
model_reasoning_effort = "high"
|
model_reasoning_effort = "high"
|
||||||
sandbox_mode = "workspace-write"
|
sandbox_mode = "workspace-write"
|
||||||
|
|
||||||
developer_instructions = """
|
developer_instructions = '''
|
||||||
|
|
||||||
You are a code execution specialist focused on implementing high-quality, production-ready code. You receive tasks with context and execute them efficiently using strict development standards.
|
You are a code execution specialist focused on implementing high-quality, production-ready code. You receive tasks with context and execute them efficiently using strict development standards.
|
||||||
|
|
||||||
@@ -505,4 +505,4 @@ Before completing any task, verify:
|
|||||||
- Document all new interfaces, types, and constants for dependent task reference
|
- Document all new interfaces, types, and constants for dependent task reference
|
||||||
### Windows Path Format Guidelines
|
### Windows Path Format Guidelines
|
||||||
- **Quick Ref**: `C:\Users` → MCP: `C:\\Users` | Bash: `/c/Users` or `C:/Users`
|
- **Quick Ref**: `C:\Users` → MCP: `C:\\Users` | Bash: `/c/Users` or `C:/Users`
|
||||||
"""
|
'''
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ model = "gpt-5.4"
|
|||||||
model_reasoning_effort = "high"
|
model_reasoning_effort = "high"
|
||||||
sandbox_mode = "read-only"
|
sandbox_mode = "read-only"
|
||||||
|
|
||||||
developer_instructions = """
|
developer_instructions = '''
|
||||||
|
|
||||||
You are a context discovery specialist focused on gathering relevant project information for development tasks. Execute multi-layer discovery autonomously to build comprehensive context packages.
|
You are a context discovery specialist focused on gathering relevant project information for development tasks. Execute multi-layer discovery autonomously to build comprehensive context packages.
|
||||||
|
|
||||||
@@ -577,4 +577,4 @@ Output: .workflow/session/{session}/.process/context-package.json
|
|||||||
### Windows Path Format Guidelines
|
### Windows Path Format Guidelines
|
||||||
- **Quick Ref**: `C:\Users` → MCP: `C:\\Users` | Bash: `/c/Users` or `C:/Users`
|
- **Quick Ref**: `C:\Users` → MCP: `C:\\Users` | Bash: `/c/Users` or `C:/Users`
|
||||||
- **Context Package**: Use project-relative paths (e.g., `src/auth/service.ts`)
|
- **Context Package**: Use project-relative paths (e.g., `src/auth/service.ts`)
|
||||||
"""
|
'''
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ model = "gpt-5.4"
|
|||||||
model_reasoning_effort = "high"
|
model_reasoning_effort = "high"
|
||||||
sandbox_mode = "workspace-write"
|
sandbox_mode = "workspace-write"
|
||||||
|
|
||||||
developer_instructions = """
|
developer_instructions = '''
|
||||||
|
|
||||||
You are an intelligent debugging specialist that autonomously diagnoses bugs through evidence-based hypothesis testing and CLI-assisted analysis.
|
You are an intelligent debugging specialist that autonomously diagnoses bugs through evidence-based hypothesis testing and CLI-assisted analysis.
|
||||||
|
|
||||||
@@ -434,4 +434,4 @@ ${nextSteps}
|
|||||||
- Timeout: Analysis 20min | Fix implementation 40min
|
- Timeout: Analysis 20min | Fix implementation 40min
|
||||||
|
|
||||||
---
|
---
|
||||||
"""
|
'''
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ model = "gpt-5.4"
|
|||||||
model_reasoning_effort = "high"
|
model_reasoning_effort = "high"
|
||||||
sandbox_mode = "workspace-write"
|
sandbox_mode = "workspace-write"
|
||||||
|
|
||||||
developer_instructions = """
|
developer_instructions = '''
|
||||||
|
|
||||||
You are an expert technical documentation specialist. Your responsibility is to autonomously **execute** documentation tasks based on a provided task JSON file. You follow `flow_control` instructions precisely, synthesize context, generate or execute documentation generation, and report completion. You do not make planning decisions.
|
You are an expert technical documentation specialist. Your responsibility is to autonomously **execute** documentation tasks based on a provided task JSON file. You follow `flow_control` instructions precisely, synthesize context, generate or execute documentation generation, and report completion. You do not make planning decisions.
|
||||||
|
|
||||||
@@ -322,4 +322,4 @@ Before completing the task, you must verify the following:
|
|||||||
- **Generate Code**: Your role is to document, not to implement.
|
- **Generate Code**: Your role is to document, not to implement.
|
||||||
- **Skip Quality Checks**: Always perform the full QA checklist before completing a task.
|
- **Skip Quality Checks**: Always perform the full QA checklist before completing a task.
|
||||||
- **Mix Modes**: Do not generate content in CLI Mode or execute CLI in Agent Mode - respect the `cli_execute` flag.
|
- **Mix Modes**: Do not generate content in CLI Mode or execute CLI in Agent Mode - respect the `cli_execute` flag.
|
||||||
"""
|
'''
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ model = "gpt-5.4"
|
|||||||
model_reasoning_effort = "high"
|
model_reasoning_effort = "high"
|
||||||
sandbox_mode = "workspace-write"
|
sandbox_mode = "workspace-write"
|
||||||
|
|
||||||
developer_instructions = """
|
developer_instructions = '''
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -309,4 +309,4 @@ Return brief summaries; full conflict details in separate files:
|
|||||||
```
|
```
|
||||||
- `clarifications`: Only present if unresolved high-severity conflicts exist
|
- `clarifications`: Only present if unresolved high-severity conflicts exist
|
||||||
- No markdown, no prose - PURE JSON only
|
- No markdown, no prose - PURE JSON only
|
||||||
"""
|
'''
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ model = "gpt-5.4"
|
|||||||
model_reasoning_effort = "high"
|
model_reasoning_effort = "high"
|
||||||
sandbox_mode = "workspace-write"
|
sandbox_mode = "workspace-write"
|
||||||
|
|
||||||
developer_instructions = """
|
developer_instructions = '''
|
||||||
|
|
||||||
## Agent Inheritance
|
## Agent Inheritance
|
||||||
|
|
||||||
@@ -673,4 +673,4 @@ Hard Constraints:
|
|||||||
- AI issue detection configured in IMPL-001.3
|
- AI issue detection configured in IMPL-001.3
|
||||||
- Quality gates with measurable thresholds in IMPL-001.5
|
- Quality gates with measurable thresholds in IMPL-001.5
|
||||||
- Source session status reported (if applicable)
|
- Source session status reported (if applicable)
|
||||||
"""
|
'''
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ model = "gpt-5.4"
|
|||||||
model_reasoning_effort = "high"
|
model_reasoning_effort = "high"
|
||||||
sandbox_mode = "workspace-write"
|
sandbox_mode = "workspace-write"
|
||||||
|
|
||||||
developer_instructions = """
|
developer_instructions = '''
|
||||||
|
|
||||||
You are a specialized **Test Execution & Fix Agent**. Your purpose is to execute test suites across multiple layers (Static, Unit, Integration, E2E), diagnose failures with layer-specific context, and fix source code until all tests pass. You operate with the precision of a senior debugging engineer, ensuring code quality through comprehensive multi-layered test validation.
|
You are a specialized **Test Execution & Fix Agent**. Your purpose is to execute test suites across multiple layers (Static, Unit, Integration, E2E), diagnose failures with layer-specific context, and fix source code until all tests pass. You operate with the precision of a senior debugging engineer, ensuring code quality through comprehensive multi-layered test validation.
|
||||||
|
|
||||||
@@ -354,4 +354,4 @@ jq --arg ts "$(date -Iseconds)" '.status="completed" | .status_history += [{"fro
|
|||||||
**Tests passing = Code approved = Mission complete** ✅
|
**Tests passing = Code approved = Mission complete** ✅
|
||||||
### Windows Path Format Guidelines
|
### Windows Path Format Guidelines
|
||||||
- **Quick Ref**: `C:\Users` → MCP: `C:\\Users` | Bash: `/c/Users` or `C:/Users`
|
- **Quick Ref**: `C:\Users` → MCP: `C:\\Users` | Bash: `/c/Users` or `C:/Users`
|
||||||
"""
|
'''
|
||||||
|
|||||||
305
ccw/frontend/src/components/settings/AgentDefinitionsSection.tsx
Normal file
305
ccw/frontend/src/components/settings/AgentDefinitionsSection.tsx
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
// ========================================
|
||||||
|
// Agent Definitions Section
|
||||||
|
// ========================================
|
||||||
|
// Settings section for viewing and editing Codex/Claude agent model and effort fields
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { Bot, ChevronDown, ChevronRight, Save, RefreshCw } from 'lucide-react';
|
||||||
|
import { Card } from '@/components/ui/Card';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Input } from '@/components/ui/Input';
|
||||||
|
import { Badge } from '@/components/ui/Badge';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import {
|
||||||
|
fetchAgentDefinitions,
|
||||||
|
updateAgentDefinition,
|
||||||
|
batchUpdateAgentDefinitions,
|
||||||
|
type AgentDefinition,
|
||||||
|
} from '@/lib/api';
|
||||||
|
|
||||||
|
// ========== Effort options ==========
|
||||||
|
|
||||||
|
const CODEX_EFFORTS = ['', 'low', 'medium', 'high'];
|
||||||
|
const CLAUDE_EFFORTS = ['', 'low', 'medium', 'high', 'max'];
|
||||||
|
const CLAUDE_MODEL_PRESETS = ['sonnet', 'opus', 'haiku', 'inherit'];
|
||||||
|
|
||||||
|
// ========== Agent Card ==========
|
||||||
|
|
||||||
|
interface AgentCardProps {
|
||||||
|
agent: AgentDefinition;
|
||||||
|
onSaved: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AgentCard({ agent, onSaved }: AgentCardProps) {
|
||||||
|
const [model, setModel] = useState(agent.model);
|
||||||
|
const [effort, setEffort] = useState(agent.effort);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
const isDirty = model !== agent.model || effort !== agent.effort;
|
||||||
|
const effortOptions = agent.type === 'codex' ? CODEX_EFFORTS : CLAUDE_EFFORTS;
|
||||||
|
|
||||||
|
const handleSave = useCallback(async () => {
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const body: { filePath: string; model?: string; effort?: string } = { filePath: agent.filePath };
|
||||||
|
if (model !== agent.model) body.model = model;
|
||||||
|
if (effort !== agent.effort) body.effort = effort;
|
||||||
|
await updateAgentDefinition(agent.type, agent.name, body);
|
||||||
|
toast.success(`Updated ${agent.name}`);
|
||||||
|
onSaved();
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(`Failed to update ${agent.name}: ${(err as Error).message}`);
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}, [agent, model, effort, onSaved]);
|
||||||
|
|
||||||
|
// Sync local state when agent prop changes (after refetch)
|
||||||
|
useEffect(() => {
|
||||||
|
setModel(agent.model);
|
||||||
|
setEffort(agent.effort);
|
||||||
|
}, [agent.model, agent.effort]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3 py-2 px-3 rounded-md border border-border bg-card hover:bg-accent/30 transition-colors">
|
||||||
|
<Badge variant={agent.type === 'codex' ? 'default' : 'secondary'} className="text-xs shrink-0">
|
||||||
|
{agent.type}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-sm font-medium text-foreground min-w-[140px] truncate" title={agent.name}>
|
||||||
|
{agent.name}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Model input */}
|
||||||
|
<div className="flex items-center gap-1 flex-1 min-w-0">
|
||||||
|
<span className="text-xs text-muted-foreground shrink-0">Model:</span>
|
||||||
|
{agent.type === 'claude' ? (
|
||||||
|
<div className="flex gap-1 flex-1 min-w-0">
|
||||||
|
<select
|
||||||
|
className="h-7 text-xs rounded border border-input bg-background px-2 shrink-0"
|
||||||
|
value={CLAUDE_MODEL_PRESETS.includes(model) ? model : '__custom__'}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.value === '__custom__') return;
|
||||||
|
setModel(e.target.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{CLAUDE_MODEL_PRESETS.map(m => (
|
||||||
|
<option key={m} value={m}>{m || '(none)'}</option>
|
||||||
|
))}
|
||||||
|
{!CLAUDE_MODEL_PRESETS.includes(model) && (
|
||||||
|
<option value="__custom__">custom</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
{!CLAUDE_MODEL_PRESETS.includes(model) && (
|
||||||
|
<Input
|
||||||
|
className="h-7 text-xs flex-1 min-w-[100px]"
|
||||||
|
value={model}
|
||||||
|
onChange={(e) => setModel(e.target.value)}
|
||||||
|
placeholder="model id"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
className="h-7 text-xs flex-1 min-w-[100px]"
|
||||||
|
value={model}
|
||||||
|
onChange={(e) => setModel(e.target.value)}
|
||||||
|
placeholder="model id"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Effort select */}
|
||||||
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
|
<span className="text-xs text-muted-foreground">Effort:</span>
|
||||||
|
<select
|
||||||
|
className="h-7 text-xs rounded border border-input bg-background px-2"
|
||||||
|
value={effort}
|
||||||
|
onChange={(e) => setEffort(e.target.value)}
|
||||||
|
>
|
||||||
|
{effortOptions.map(e => (
|
||||||
|
<option key={e} value={e}>{e || '—'}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Save button */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-2 shrink-0"
|
||||||
|
disabled={!isDirty || saving}
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
<Save className="w-3 h-3 mr-1" />
|
||||||
|
{saving ? '...' : 'Save'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Installation Group ==========
|
||||||
|
|
||||||
|
interface InstallationGroupProps {
|
||||||
|
installationPath: string;
|
||||||
|
agents: AgentDefinition[];
|
||||||
|
onSaved: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function InstallationGroup({ installationPath, agents, onSaved }: InstallationGroupProps) {
|
||||||
|
const [expanded, setExpanded] = useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-2 w-full text-left py-1 hover:text-primary transition-colors"
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
>
|
||||||
|
{expanded ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
|
||||||
|
<span className="text-sm font-medium text-foreground truncate" title={installationPath}>
|
||||||
|
{installationPath}
|
||||||
|
</span>
|
||||||
|
<Badge variant="outline" className="text-xs ml-auto shrink-0">
|
||||||
|
{agents.length} agents
|
||||||
|
</Badge>
|
||||||
|
</button>
|
||||||
|
{expanded && (
|
||||||
|
<div className="space-y-1 ml-6">
|
||||||
|
{agents.map((agent) => (
|
||||||
|
<AgentCard key={agent.filePath} agent={agent} onSaved={onSaved} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Main Component ==========
|
||||||
|
|
||||||
|
export function AgentDefinitionsSection() {
|
||||||
|
const [agents, setAgents] = useState<AgentDefinition[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [batchModel, setBatchModel] = useState('');
|
||||||
|
const [batchEffort, setBatchEffort] = useState('');
|
||||||
|
const [batchSaving, setBatchSaving] = useState(false);
|
||||||
|
|
||||||
|
const loadAgents = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const data = await fetchAgentDefinitions();
|
||||||
|
setAgents(data.agents);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(`Failed to load agents: ${(err as Error).message}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => { loadAgents(); }, [loadAgents]);
|
||||||
|
|
||||||
|
// Group agents by installation path
|
||||||
|
const grouped = agents.reduce<Record<string, AgentDefinition[]>>((acc, agent) => {
|
||||||
|
const key = agent.installationPath;
|
||||||
|
if (!acc[key]) acc[key] = [];
|
||||||
|
acc[key].push(agent);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const handleBatchApply = useCallback(async () => {
|
||||||
|
if (!batchModel && !batchEffort) {
|
||||||
|
toast.error('Set a model or effort value first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBatchSaving(true);
|
||||||
|
try {
|
||||||
|
const targets = agents.map(a => ({ filePath: a.filePath, type: a.type }));
|
||||||
|
const result = await batchUpdateAgentDefinitions({
|
||||||
|
targets,
|
||||||
|
model: batchModel || undefined,
|
||||||
|
effort: batchEffort || undefined,
|
||||||
|
});
|
||||||
|
toast.success(`Updated ${result.updated}/${result.total} agents`);
|
||||||
|
loadAgents();
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(`Batch update failed: ${(err as Error).message}`);
|
||||||
|
} finally {
|
||||||
|
setBatchSaving(false);
|
||||||
|
}
|
||||||
|
}, [agents, batchModel, batchEffort, loadAgents]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||||
|
<Bot className="w-5 h-5" />
|
||||||
|
Agent Definitions
|
||||||
|
</h2>
|
||||||
|
<Button variant="ghost" size="sm" onClick={loadAgents} disabled={loading}>
|
||||||
|
<RefreshCw className={cn('w-4 h-4 mr-1', loading && 'animate-spin')} />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Batch Controls */}
|
||||||
|
<div className="flex items-center gap-3 p-3 mb-4 rounded-md border border-border bg-muted/30">
|
||||||
|
<span className="text-xs font-medium text-muted-foreground shrink-0">Batch:</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-xs text-muted-foreground">Model:</span>
|
||||||
|
<Input
|
||||||
|
className="h-7 text-xs w-[160px]"
|
||||||
|
value={batchModel}
|
||||||
|
onChange={(e) => setBatchModel(e.target.value)}
|
||||||
|
placeholder="model for all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-xs text-muted-foreground">Effort:</span>
|
||||||
|
<select
|
||||||
|
className="h-7 text-xs rounded border border-input bg-background px-2"
|
||||||
|
value={batchEffort}
|
||||||
|
onChange={(e) => setBatchEffort(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">—</option>
|
||||||
|
<option value="low">low</option>
|
||||||
|
<option value="medium">medium</option>
|
||||||
|
<option value="high">high</option>
|
||||||
|
<option value="max">max (claude only)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
className="h-7"
|
||||||
|
disabled={batchSaving || (!batchModel && !batchEffort)}
|
||||||
|
onClick={handleBatchApply}
|
||||||
|
>
|
||||||
|
{batchSaving ? 'Applying...' : 'Apply to All'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Agent list */}
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center text-sm text-muted-foreground py-8">Loading agents...</div>
|
||||||
|
) : agents.length === 0 ? (
|
||||||
|
<div className="text-center text-sm text-muted-foreground py-8">
|
||||||
|
No agent definitions found. Install CCW to a project first.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{Object.entries(grouped).map(([path, groupAgents]) => (
|
||||||
|
<InstallationGroup
|
||||||
|
key={path}
|
||||||
|
installationPath={path}
|
||||||
|
agents={groupAgents}
|
||||||
|
onSaved={loadAgents}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AgentDefinitionsSection;
|
||||||
@@ -6203,6 +6203,42 @@ export async function importSettings(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Agent Definitions API ==========
|
||||||
|
|
||||||
|
export interface AgentDefinition {
|
||||||
|
name: string;
|
||||||
|
type: 'codex' | 'claude';
|
||||||
|
filePath: string;
|
||||||
|
installationPath: string;
|
||||||
|
model: string;
|
||||||
|
effort: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAgentDefinitions(): Promise<{ agents: AgentDefinition[] }> {
|
||||||
|
return fetchApi('/api/agent-definitions');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAgentDefinition(
|
||||||
|
type: 'codex' | 'claude',
|
||||||
|
name: string,
|
||||||
|
body: { filePath: string; model?: string; effort?: string }
|
||||||
|
): Promise<{ success: boolean; name: string; type: string; model?: string; effort?: string }> {
|
||||||
|
return fetchApi(`/api/agent-definitions/${type}/${encodeURIComponent(name)}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function batchUpdateAgentDefinitions(
|
||||||
|
body: { targets: Array<{ filePath: string; type: 'codex' | 'claude' }>; model?: string; effort?: string }
|
||||||
|
): Promise<{ success: boolean; updated: number; total: number; results: Array<{ filePath: string; success: boolean; error?: string }> }> {
|
||||||
|
return fetchApi('/api/agent-definitions/batch', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ========== CCW Tools API ==========
|
// ========== CCW Tools API ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import {
|
|||||||
import type { ExportedSettings } from '@/lib/api';
|
import type { ExportedSettings } from '@/lib/api';
|
||||||
import { RemoteNotificationSection } from '@/components/settings/RemoteNotificationSection';
|
import { RemoteNotificationSection } from '@/components/settings/RemoteNotificationSection';
|
||||||
import { A2UIPreferencesSection } from '@/components/settings/A2UIPreferencesSection';
|
import { A2UIPreferencesSection } from '@/components/settings/A2UIPreferencesSection';
|
||||||
|
import { AgentDefinitionsSection } from '@/components/settings/AgentDefinitionsSection';
|
||||||
|
|
||||||
// ========== CSRF Token Helper ==========
|
// ========== CSRF Token Helper ==========
|
||||||
function getCsrfToken(): string | null {
|
function getCsrfToken(): string | null {
|
||||||
@@ -1478,6 +1479,9 @@ export function SettingsPage() {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Agent Definitions */}
|
||||||
|
<AgentDefinitionsSection />
|
||||||
|
|
||||||
{/* Data Refresh Settings */}
|
{/* Data Refresh Settings */}
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2 mb-4">
|
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2 mb-4">
|
||||||
|
|||||||
315
ccw/src/core/routes/agent-definitions-routes.ts
Normal file
315
ccw/src/core/routes/agent-definitions-routes.ts
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
/**
|
||||||
|
* Agent Definitions Routes Module
|
||||||
|
* Handles discovery, viewing, and editing of Codex (.toml) and Claude (.md) agent definitions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
|
||||||
|
import { join, basename } from 'path';
|
||||||
|
|
||||||
|
import type { RouteContext } from './types.js';
|
||||||
|
import { getAllManifests } from '../manifest.js';
|
||||||
|
|
||||||
|
// ========== Types ==========
|
||||||
|
|
||||||
|
interface AgentDefinition {
|
||||||
|
name: string;
|
||||||
|
type: 'codex' | 'claude';
|
||||||
|
filePath: string;
|
||||||
|
installationPath: string;
|
||||||
|
model: string;
|
||||||
|
effort: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Parsing helpers ==========
|
||||||
|
|
||||||
|
function parseCodexToml(content: string, filePath: string, installationPath: string): AgentDefinition | null {
|
||||||
|
const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
|
||||||
|
const modelMatch = content.match(/^model\s*=\s*"([^"]+)"/m);
|
||||||
|
const effortMatch = content.match(/^model_reasoning_effort\s*=\s*"([^"]+)"/m);
|
||||||
|
const descMatch = content.match(/^description\s*=\s*"([^"]+)"/m);
|
||||||
|
|
||||||
|
if (!nameMatch) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: nameMatch[1],
|
||||||
|
type: 'codex',
|
||||||
|
filePath,
|
||||||
|
installationPath,
|
||||||
|
model: modelMatch?.[1] ?? '',
|
||||||
|
effort: effortMatch?.[1] ?? '',
|
||||||
|
description: descMatch?.[1] ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseClaudeMd(content: string, filePath: string, installationPath: string): AgentDefinition | null {
|
||||||
|
// Extract YAML frontmatter between --- delimiters
|
||||||
|
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||||
|
if (!fmMatch) return null;
|
||||||
|
|
||||||
|
const fm = fmMatch[1];
|
||||||
|
const nameMatch = fm.match(/^name:\s*(.+)$/m);
|
||||||
|
const modelMatch = fm.match(/^model:\s*(.+)$/m);
|
||||||
|
const effortMatch = fm.match(/^effort:\s*(.+)$/m);
|
||||||
|
// description can be multi-line with |, just grab first line
|
||||||
|
const descMatch = fm.match(/^description:\s*\|?\s*\n?\s*(.+)$/m);
|
||||||
|
|
||||||
|
if (!nameMatch) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: nameMatch[1].trim(),
|
||||||
|
type: 'claude',
|
||||||
|
filePath,
|
||||||
|
installationPath,
|
||||||
|
model: modelMatch?.[1].trim() ?? '',
|
||||||
|
effort: effortMatch?.[1].trim() ?? '',
|
||||||
|
description: descMatch?.[1].trim() ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Discovery ==========
|
||||||
|
|
||||||
|
function scanAgentsInPath(instPath: string, agents: AgentDefinition[]): void {
|
||||||
|
// Scan .codex/agents/*.toml
|
||||||
|
const codexDir = join(instPath, '.codex', 'agents');
|
||||||
|
if (existsSync(codexDir)) {
|
||||||
|
try {
|
||||||
|
const files = readdirSync(codexDir).filter(f => f.endsWith('.toml'));
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = join(codexDir, file);
|
||||||
|
try {
|
||||||
|
const content = readFileSync(filePath, 'utf-8');
|
||||||
|
const agent = parseCodexToml(content, filePath, instPath);
|
||||||
|
if (agent) agents.push(agent);
|
||||||
|
} catch { /* skip unreadable */ }
|
||||||
|
}
|
||||||
|
} catch { /* skip unreadable dir */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan .claude/agents/*.md
|
||||||
|
const claudeDir = join(instPath, '.claude', 'agents');
|
||||||
|
if (existsSync(claudeDir)) {
|
||||||
|
try {
|
||||||
|
const files = readdirSync(claudeDir).filter(f => f.endsWith('.md'));
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = join(claudeDir, file);
|
||||||
|
try {
|
||||||
|
const content = readFileSync(filePath, 'utf-8');
|
||||||
|
const agent = parseClaudeMd(content, filePath, instPath);
|
||||||
|
if (agent) agents.push(agent);
|
||||||
|
} catch { /* skip unreadable */ }
|
||||||
|
}
|
||||||
|
} catch { /* skip unreadable dir */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function discoverAgents(initialPath: string): AgentDefinition[] {
|
||||||
|
const manifests = getAllManifests();
|
||||||
|
const agents: AgentDefinition[] = [];
|
||||||
|
const scannedPaths = new Set<string>();
|
||||||
|
|
||||||
|
// Scan manifest installation paths
|
||||||
|
for (const manifest of manifests) {
|
||||||
|
const normalized = manifest.installation_path.toLowerCase().replace(/[\\/]+$/, '');
|
||||||
|
if (!scannedPaths.has(normalized)) {
|
||||||
|
scannedPaths.add(normalized);
|
||||||
|
scanAgentsInPath(manifest.installation_path, agents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also scan initialPath (server CWD / project root) if not already covered
|
||||||
|
const normalizedInitial = initialPath.toLowerCase().replace(/[\\/]+$/, '');
|
||||||
|
if (!scannedPaths.has(normalizedInitial)) {
|
||||||
|
scannedPaths.add(normalizedInitial);
|
||||||
|
scanAgentsInPath(initialPath, agents);
|
||||||
|
}
|
||||||
|
|
||||||
|
return agents;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== File update helpers (surgical regex) ==========
|
||||||
|
|
||||||
|
function updateCodexTomlField(content: string, field: string, value: string): string {
|
||||||
|
const regex = new RegExp(`^${field}\\s*=\\s*"[^"]*"`, 'm');
|
||||||
|
if (regex.test(content)) {
|
||||||
|
return content.replace(regex, `${field} = "${value}"`);
|
||||||
|
}
|
||||||
|
// Insert after description line if exists, otherwise after first line
|
||||||
|
const descRegex = /^description\s*=\s*"[^"]*"/m;
|
||||||
|
if (descRegex.test(content)) {
|
||||||
|
return content.replace(descRegex, (match) => `${match}\n${field} = "${value}"`);
|
||||||
|
}
|
||||||
|
// Fallback: append after first non-empty line
|
||||||
|
const lines = content.split('\n');
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (lines[i].trim().length > 0) {
|
||||||
|
lines.splice(i + 1, 0, `${field} = "${value}"`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateClaudeMdField(content: string, field: string, value: string): string {
|
||||||
|
const fmMatch = content.match(/^(---\r?\n)([\s\S]*?)(\r?\n---)/);
|
||||||
|
if (!fmMatch) return content;
|
||||||
|
|
||||||
|
let fm = fmMatch[2];
|
||||||
|
const fieldRegex = new RegExp(`^${field}:\\s*.*$`, 'm');
|
||||||
|
|
||||||
|
if (fieldRegex.test(fm)) {
|
||||||
|
fm = fm.replace(fieldRegex, `${field}: ${value}`);
|
||||||
|
} else {
|
||||||
|
// Append before end of frontmatter
|
||||||
|
fm = fm.trimEnd() + `\n${field}: ${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmMatch[1] + fm + fmMatch[3] + content.slice(fmMatch[0].length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Validation ==========
|
||||||
|
|
||||||
|
const CODEX_EFFORTS = ['low', 'medium', 'high'];
|
||||||
|
const CLAUDE_EFFORTS = ['low', 'medium', 'high', 'max'];
|
||||||
|
const CLAUDE_MODEL_SHORTCUTS = ['sonnet', 'opus', 'haiku', 'inherit'];
|
||||||
|
|
||||||
|
function validateEffort(type: 'codex' | 'claude', effort: string): boolean {
|
||||||
|
if (!effort) return true; // empty = no change
|
||||||
|
return type === 'codex' ? CODEX_EFFORTS.includes(effort) : CLAUDE_EFFORTS.includes(effort);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateModel(type: 'codex' | 'claude', model: string): boolean {
|
||||||
|
if (!model) return true; // empty = no change
|
||||||
|
if (type === 'claude') {
|
||||||
|
// Allow shortcuts or full model IDs (any non-empty string)
|
||||||
|
return model.length > 0;
|
||||||
|
}
|
||||||
|
// Codex: any non-empty string
|
||||||
|
return model.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Route handler ==========
|
||||||
|
|
||||||
|
export async function handleAgentDefinitionsRoutes(ctx: RouteContext): Promise<boolean> {
|
||||||
|
const { pathname, req, res, handlePostRequest, initialPath } = ctx;
|
||||||
|
|
||||||
|
// ========== GET /api/agent-definitions ==========
|
||||||
|
if (pathname === '/api/agent-definitions' && req.method === 'GET') {
|
||||||
|
try {
|
||||||
|
const agents = discoverAgents(initialPath);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ agents }));
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: (err as Error).message }));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== PUT /api/agent-definitions/batch ==========
|
||||||
|
if (pathname === '/api/agent-definitions/batch' && req.method === 'PUT') {
|
||||||
|
handlePostRequest(req, res, async (body: unknown) => {
|
||||||
|
try {
|
||||||
|
const { targets, model, effort } = body as {
|
||||||
|
targets: Array<{ filePath: string; type: 'codex' | 'claude' }>;
|
||||||
|
model?: string;
|
||||||
|
effort?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!targets || !Array.isArray(targets) || targets.length === 0) {
|
||||||
|
return { error: 'targets array is required', status: 400 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: Array<{ filePath: string; success: boolean; error?: string }> = [];
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
try {
|
||||||
|
if (!existsSync(target.filePath)) {
|
||||||
|
results.push({ filePath: target.filePath, success: false, error: 'File not found' });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effort && !validateEffort(target.type, effort)) {
|
||||||
|
results.push({ filePath: target.filePath, success: false, error: `Invalid effort: ${effort}` });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (model && !validateModel(target.type, model)) {
|
||||||
|
results.push({ filePath: target.filePath, success: false, error: `Invalid model: ${model}` });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = readFileSync(target.filePath, 'utf-8');
|
||||||
|
|
||||||
|
if (target.type === 'codex') {
|
||||||
|
if (model) content = updateCodexTomlField(content, 'model', model);
|
||||||
|
if (effort) content = updateCodexTomlField(content, 'model_reasoning_effort', effort);
|
||||||
|
} else {
|
||||||
|
if (model) content = updateClaudeMdField(content, 'model', model);
|
||||||
|
if (effort) content = updateClaudeMdField(content, 'effort', effort);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(target.filePath, content, 'utf-8');
|
||||||
|
results.push({ filePath: target.filePath, success: true });
|
||||||
|
} catch (err) {
|
||||||
|
results.push({ filePath: target.filePath, success: false, error: (err as Error).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const successCount = results.filter(r => r.success).length;
|
||||||
|
return { success: true, updated: successCount, total: targets.length, results };
|
||||||
|
} catch (err) {
|
||||||
|
return { error: (err as Error).message, status: 500 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== PUT /api/agent-definitions/:type/:name ==========
|
||||||
|
const putMatch = pathname.match(/^\/api\/agent-definitions\/(codex|claude)\/([^/]+)$/);
|
||||||
|
if (putMatch && req.method === 'PUT') {
|
||||||
|
const agentType = putMatch[1] as 'codex' | 'claude';
|
||||||
|
const agentName = decodeURIComponent(putMatch[2]);
|
||||||
|
|
||||||
|
handlePostRequest(req, res, async (body: unknown) => {
|
||||||
|
try {
|
||||||
|
const { filePath, model, effort } = body as {
|
||||||
|
filePath: string;
|
||||||
|
model?: string;
|
||||||
|
effort?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!filePath) {
|
||||||
|
return { error: 'filePath is required', status: 400 };
|
||||||
|
}
|
||||||
|
if (!existsSync(filePath)) {
|
||||||
|
return { error: 'File not found', status: 404 };
|
||||||
|
}
|
||||||
|
if (effort && !validateEffort(agentType, effort)) {
|
||||||
|
return { error: `Invalid effort value: ${effort}. Valid: ${agentType === 'codex' ? CODEX_EFFORTS.join(', ') : CLAUDE_EFFORTS.join(', ')}`, status: 400 };
|
||||||
|
}
|
||||||
|
if (model && !validateModel(agentType, model)) {
|
||||||
|
return { error: 'Invalid model value', status: 400 };
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = readFileSync(filePath, 'utf-8');
|
||||||
|
|
||||||
|
if (agentType === 'codex') {
|
||||||
|
if (model) content = updateCodexTomlField(content, 'model', model);
|
||||||
|
if (effort) content = updateCodexTomlField(content, 'model_reasoning_effort', effort);
|
||||||
|
} else {
|
||||||
|
if (model) content = updateClaudeMdField(content, 'model', model);
|
||||||
|
if (effort) content = updateClaudeMdField(content, 'effort', effort);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(filePath, content, 'utf-8');
|
||||||
|
|
||||||
|
return { success: true, name: agentName, type: agentType, model, effort };
|
||||||
|
} catch (err) {
|
||||||
|
return { error: (err as Error).message, status: 500 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ import { handleAnalysisRoutes } from './routes/analysis-routes.js';
|
|||||||
import { handleSpecRoutes } from './routes/spec-routes.js';
|
import { handleSpecRoutes } from './routes/spec-routes.js';
|
||||||
import { handleDeepWikiRoutes } from './routes/deepwiki-routes.js';
|
import { handleDeepWikiRoutes } from './routes/deepwiki-routes.js';
|
||||||
import { handleCodexLensRoutes } from './routes/codexlens-routes.js';
|
import { handleCodexLensRoutes } from './routes/codexlens-routes.js';
|
||||||
|
import { handleAgentDefinitionsRoutes } from './routes/agent-definitions-routes.js';
|
||||||
|
|
||||||
// Import WebSocket handling
|
// Import WebSocket handling
|
||||||
import { handleWebSocketUpgrade, broadcastToClients, extractSessionIdFromPath } from './websocket.js';
|
import { handleWebSocketUpgrade, broadcastToClients, extractSessionIdFromPath } from './websocket.js';
|
||||||
@@ -517,6 +518,11 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
|||||||
if (await handleAuditRoutes(routeContext)) return;
|
if (await handleAuditRoutes(routeContext)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Agent definitions routes (/api/agent-definitions/*)
|
||||||
|
if (pathname.startsWith('/api/agent-definitions')) {
|
||||||
|
if (await handleAgentDefinitionsRoutes(routeContext)) return;
|
||||||
|
}
|
||||||
|
|
||||||
// CLI routes (/api/cli/*)
|
// CLI routes (/api/cli/*)
|
||||||
if (pathname.startsWith('/api/cli/')) {
|
if (pathname.startsWith('/api/cli/')) {
|
||||||
// CLI Settings routes first (more specific path /api/cli/settings/*)
|
// CLI Settings routes first (more specific path /api/cli/settings/*)
|
||||||
|
|||||||
Reference in New Issue
Block a user