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"
sandbox_mode = "workspace-write"
developer_instructions = """
developer_instructions = '''
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
---
"""
'''

View File

@@ -4,7 +4,7 @@ model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "read-only"
developer_instructions = """
developer_instructions = '''
You are a specialized CLI exploration agent that autonomously analyzes codebases and generates structured outputs.
@@ -229,4 +229,4 @@ Brief summary:
**Consumption Pattern**:
- Plan phase: Fully consumes `exploration-notes.md`
- 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"
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.
@@ -898,4 +898,4 @@ After Phase 4 planObject generation:
5. **Return** → Plan with `_metadata.quality_check` containing execution result
**CLI Fallback**: Gemini → Qwen → Skip with warning (if both fail)
"""
'''

View File

@@ -4,7 +4,7 @@ model = "gpt-5.4"
model_reasoning_effort = "high"
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.
@@ -550,4 +550,4 @@ See: `.process/iteration-{iteration}-cli-output.txt`
estimated_complexity: "medium"
}
```
"""
'''

View File

@@ -4,7 +4,7 @@ model = "gpt-5.4"
model_reasoning_effort = "high"
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.
@@ -505,4 +505,4 @@ Before completing any task, verify:
- Document all new interfaces, types, and constants for dependent task reference
### Windows Path Format Guidelines
- **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"
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.
@@ -577,4 +577,4 @@ Output: .workflow/session/{session}/.process/context-package.json
### Windows Path Format Guidelines
- **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`)
"""
'''

View File

@@ -4,7 +4,7 @@ model = "gpt-5.4"
model_reasoning_effort = "high"
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.
@@ -434,4 +434,4 @@ ${nextSteps}
- Timeout: Analysis 20min | Fix implementation 40min
---
"""
'''

View File

@@ -4,7 +4,7 @@ model = "gpt-5.4"
model_reasoning_effort = "high"
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.
@@ -322,4 +322,4 @@ Before completing the task, you must verify the following:
- **Generate Code**: Your role is to document, not to implement.
- **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.
"""
'''

View File

@@ -4,7 +4,7 @@ model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
developer_instructions = """
developer_instructions = '''
## Overview
@@ -309,4 +309,4 @@ Return brief summaries; full conflict details in separate files:
```
- `clarifications`: Only present if unresolved high-severity conflicts exist
- No markdown, no prose - PURE JSON only
"""
'''

View File

@@ -4,7 +4,7 @@ model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
developer_instructions = """
developer_instructions = '''
## Agent Inheritance
@@ -673,4 +673,4 @@ Hard Constraints:
- AI issue detection configured in IMPL-001.3
- Quality gates with measurable thresholds in IMPL-001.5
- Source session status reported (if applicable)
"""
'''

View File

@@ -4,7 +4,7 @@ model = "gpt-5.4"
model_reasoning_effort = "high"
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.
@@ -354,4 +354,4 @@ jq --arg ts "$(date -Iseconds)" '.status="completed" | .status_history += [{"fro
**Tests passing = Code approved = Mission complete**
### Windows Path Format Guidelines
- **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 ==========
/**

View File

@@ -62,6 +62,7 @@ import {
import type { ExportedSettings } from '@/lib/api';
import { RemoteNotificationSection } from '@/components/settings/RemoteNotificationSection';
import { A2UIPreferencesSection } from '@/components/settings/A2UIPreferencesSection';
import { AgentDefinitionsSection } from '@/components/settings/AgentDefinitionsSection';
// ========== CSRF Token Helper ==========
function getCsrfToken(): string | null {
@@ -1478,6 +1479,9 @@ export function SettingsPage() {
/>
</Card>
{/* Agent Definitions */}
<AgentDefinitionsSection />
{/* Data Refresh Settings */}
<Card className="p-6">
<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 { handleDeepWikiRoutes } from './routes/deepwiki-routes.js';
import { handleCodexLensRoutes } from './routes/codexlens-routes.js';
import { handleAgentDefinitionsRoutes } from './routes/agent-definitions-routes.js';
// Import WebSocket handling
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;
}
// Agent definitions routes (/api/agent-definitions/*)
if (pathname.startsWith('/api/agent-definitions')) {
if (await handleAgentDefinitionsRoutes(routeContext)) return;
}
// CLI routes (/api/cli/*)
if (pathname.startsWith('/api/cli/')) {
// CLI Settings routes first (more specific path /api/cli/settings/*)