From 9043a0d45389f49813a3d4ee0052a76be1fa034c Mon Sep 17 00:00:00 2001 From: catlog22 Date: Tue, 24 Mar 2026 20:22:44 +0800 Subject: [PATCH] feat: add agent definitions API for managing Codex and Claude agent configurations --- .codex/agents/cli-execution-agent.toml | 4 +- .codex/agents/cli-explore-agent.toml | 4 +- .codex/agents/cli-lite-planning-agent.toml | 4 +- .codex/agents/cli-planning-agent.toml | 4 +- .codex/agents/code-developer.toml | 4 +- .codex/agents/context-search-agent.toml | 4 +- .codex/agents/debug-explore-agent.toml | 4 +- .codex/agents/doc-generator.toml | 4 +- .codex/agents/issue-queue-agent.toml | 4 +- .codex/agents/test-action-planning-agent.toml | 4 +- .codex/agents/test-fix-agent.toml | 4 +- 11.md | 0 .../settings/AgentDefinitionsSection.tsx | 305 +++++++++++++++++ ccw/frontend/src/lib/api.ts | 36 ++ ccw/frontend/src/pages/SettingsPage.tsx | 4 + .../core/routes/agent-definitions-routes.ts | 315 ++++++++++++++++++ ccw/src/core/server.ts | 6 + 17 files changed, 688 insertions(+), 22 deletions(-) create mode 100644 11.md create mode 100644 ccw/frontend/src/components/settings/AgentDefinitionsSection.tsx create mode 100644 ccw/src/core/routes/agent-definitions-routes.ts diff --git a/.codex/agents/cli-execution-agent.toml b/.codex/agents/cli-execution-agent.toml index 1aaa9853..84128e5e 100644 --- a/.codex/agents/cli-execution-agent.toml +++ b/.codex/agents/cli-execution-agent.toml @@ -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 --- -""" +''' diff --git a/.codex/agents/cli-explore-agent.toml b/.codex/agents/cli-explore-agent.toml index a6afe95f..17227dbf 100644 --- a/.codex/agents/cli-explore-agent.toml +++ b/.codex/agents/cli-explore-agent.toml @@ -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 -""" +''' diff --git a/.codex/agents/cli-lite-planning-agent.toml b/.codex/agents/cli-lite-planning-agent.toml index 4db93934..cb3f838d 100644 --- a/.codex/agents/cli-lite-planning-agent.toml +++ b/.codex/agents/cli-lite-planning-agent.toml @@ -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) -""" +''' diff --git a/.codex/agents/cli-planning-agent.toml b/.codex/agents/cli-planning-agent.toml index 15fc9b4a..2858130a 100644 --- a/.codex/agents/cli-planning-agent.toml +++ b/.codex/agents/cli-planning-agent.toml @@ -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" } ``` -""" +''' diff --git a/.codex/agents/code-developer.toml b/.codex/agents/code-developer.toml index afc548bd..6747e64a 100644 --- a/.codex/agents/code-developer.toml +++ b/.codex/agents/code-developer.toml @@ -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` -""" +''' diff --git a/.codex/agents/context-search-agent.toml b/.codex/agents/context-search-agent.toml index b16664bb..30f423ae 100644 --- a/.codex/agents/context-search-agent.toml +++ b/.codex/agents/context-search-agent.toml @@ -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`) -""" +''' diff --git a/.codex/agents/debug-explore-agent.toml b/.codex/agents/debug-explore-agent.toml index bd560b64..575d2b96 100644 --- a/.codex/agents/debug-explore-agent.toml +++ b/.codex/agents/debug-explore-agent.toml @@ -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 --- -""" +''' diff --git a/.codex/agents/doc-generator.toml b/.codex/agents/doc-generator.toml index 8a21bef9..e6301888 100644 --- a/.codex/agents/doc-generator.toml +++ b/.codex/agents/doc-generator.toml @@ -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. -""" +''' diff --git a/.codex/agents/issue-queue-agent.toml b/.codex/agents/issue-queue-agent.toml index 5e86dd14..3e8adead 100644 --- a/.codex/agents/issue-queue-agent.toml +++ b/.codex/agents/issue-queue-agent.toml @@ -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 -""" +''' diff --git a/.codex/agents/test-action-planning-agent.toml b/.codex/agents/test-action-planning-agent.toml index 4e78edda..0bd31908 100644 --- a/.codex/agents/test-action-planning-agent.toml +++ b/.codex/agents/test-action-planning-agent.toml @@ -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) -""" +''' diff --git a/.codex/agents/test-fix-agent.toml b/.codex/agents/test-fix-agent.toml index fb756e46..c4d0fc3e 100644 --- a/.codex/agents/test-fix-agent.toml +++ b/.codex/agents/test-fix-agent.toml @@ -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` -""" +''' diff --git a/11.md b/11.md new file mode 100644 index 00000000..e69de29b diff --git a/ccw/frontend/src/components/settings/AgentDefinitionsSection.tsx b/ccw/frontend/src/components/settings/AgentDefinitionsSection.tsx new file mode 100644 index 00000000..6ff22624 --- /dev/null +++ b/ccw/frontend/src/components/settings/AgentDefinitionsSection.tsx @@ -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 ( +
+ + {agent.type} + + + {agent.name} + + + {/* Model input */} +
+ Model: + {agent.type === 'claude' ? ( +
+ + {!CLAUDE_MODEL_PRESETS.includes(model) && ( + setModel(e.target.value)} + placeholder="model id" + /> + )} +
+ ) : ( + setModel(e.target.value)} + placeholder="model id" + /> + )} +
+ + {/* Effort select */} +
+ Effort: + +
+ + {/* Save button */} + +
+ ); +} + +// ========== Installation Group ========== + +interface InstallationGroupProps { + installationPath: string; + agents: AgentDefinition[]; + onSaved: () => void; +} + +function InstallationGroup({ installationPath, agents, onSaved }: InstallationGroupProps) { + const [expanded, setExpanded] = useState(true); + + return ( +
+ + {expanded && ( +
+ {agents.map((agent) => ( + + ))} +
+ )} +
+ ); +} + +// ========== Main Component ========== + +export function AgentDefinitionsSection() { + const [agents, setAgents] = useState([]); + 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>((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 ( + +
+

+ + Agent Definitions +

+ +
+ + {/* Batch Controls */} +
+ Batch: +
+ Model: + setBatchModel(e.target.value)} + placeholder="model for all" + /> +
+
+ Effort: + +
+ +
+ + {/* Agent list */} + {loading ? ( +
Loading agents...
+ ) : agents.length === 0 ? ( +
+ No agent definitions found. Install CCW to a project first. +
+ ) : ( +
+ {Object.entries(grouped).map(([path, groupAgents]) => ( + + ))} +
+ )} +
+ ); +} + +export default AgentDefinitionsSection; diff --git a/ccw/frontend/src/lib/api.ts b/ccw/frontend/src/lib/api.ts index 95596080..28125e51 100644 --- a/ccw/frontend/src/lib/api.ts +++ b/ccw/frontend/src/lib/api.ts @@ -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 ========== /** diff --git a/ccw/frontend/src/pages/SettingsPage.tsx b/ccw/frontend/src/pages/SettingsPage.tsx index 3c8261be..f184aff0 100644 --- a/ccw/frontend/src/pages/SettingsPage.tsx +++ b/ccw/frontend/src/pages/SettingsPage.tsx @@ -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() { /> + {/* Agent Definitions */} + + {/* Data Refresh Settings */}

diff --git a/ccw/src/core/routes/agent-definitions-routes.ts b/ccw/src/core/routes/agent-definitions-routes.ts new file mode 100644 index 00000000..ab0a438c --- /dev/null +++ b/ccw/src/core/routes/agent-definitions-routes.ts @@ -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(); + + // 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 { + 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; +} diff --git a/ccw/src/core/server.ts b/ccw/src/core/server.ts index 35098753..b685e6c1 100644 --- a/ccw/src/core/server.ts +++ b/ccw/src/core/server.ts @@ -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