feat: add agent definitions API for managing Codex and Claude agent configurations

This commit is contained in:
catlog22
2026-03-24 20:22:44 +08:00
parent 2a6df97293
commit 9043a0d453
17 changed files with 688 additions and 22 deletions

View File

@@ -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
--- ---
""" '''

View File

@@ -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
""" '''

View File

@@ -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)
""" '''

View File

@@ -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"
} }
``` ```
""" '''

View File

@@ -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`
""" '''

View File

@@ -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`)
""" '''

View File

@@ -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
--- ---
""" '''

View File

@@ -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.
""" '''

View File

@@ -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
""" '''

View File

@@ -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)
""" '''

View File

@@ -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`
""" '''

0
11.md Normal file
View File

View 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;

View File

@@ -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 ==========
/** /**

View File

@@ -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">

View 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;
}

View File

@@ -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/*)