mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
Merge branch 'v0.9.0rc' into opencode-support
This commit is contained in:
3
.claude/.gitignore
vendored
3
.claude/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
hans/
|
hans/
|
||||||
|
skills/
|
||||||
257
apps/server/src/lib/agent-discovery.ts
Normal file
257
apps/server/src/lib/agent-discovery.ts
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
/**
|
||||||
|
* Agent Discovery - Scans filesystem for AGENT.md files
|
||||||
|
*
|
||||||
|
* Discovers agents from:
|
||||||
|
* - ~/.claude/agents/ (user-level, global)
|
||||||
|
* - .claude/agents/ (project-level)
|
||||||
|
*
|
||||||
|
* Similar to Skills, but for custom subagents defined in AGENT.md files.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
import { createLogger } from '@automaker/utils';
|
||||||
|
import { secureFs, systemPaths } from '@automaker/platform';
|
||||||
|
import type { AgentDefinition } from '@automaker/types';
|
||||||
|
|
||||||
|
const logger = createLogger('AgentDiscovery');
|
||||||
|
|
||||||
|
export interface FilesystemAgent {
|
||||||
|
name: string; // Directory name (e.g., 'code-reviewer')
|
||||||
|
definition: AgentDefinition;
|
||||||
|
source: 'user' | 'project';
|
||||||
|
filePath: string; // Full path to AGENT.md
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse agent content string into AgentDefinition
|
||||||
|
* Format:
|
||||||
|
* ---
|
||||||
|
* name: agent-name # Optional
|
||||||
|
* description: When to use this agent
|
||||||
|
* tools: tool1, tool2, tool3 # Optional (comma or space separated list)
|
||||||
|
* model: sonnet # Optional: sonnet, opus, haiku
|
||||||
|
* ---
|
||||||
|
* System prompt content here...
|
||||||
|
*/
|
||||||
|
function parseAgentContent(content: string, filePath: string): AgentDefinition | null {
|
||||||
|
// Extract frontmatter
|
||||||
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
||||||
|
if (!frontmatterMatch) {
|
||||||
|
logger.warn(`Invalid agent file format (missing frontmatter): ${filePath}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, frontmatter, prompt] = frontmatterMatch;
|
||||||
|
|
||||||
|
// Parse description (required)
|
||||||
|
const description = frontmatter.match(/description:\s*(.+)/)?.[1]?.trim();
|
||||||
|
if (!description) {
|
||||||
|
logger.warn(`Missing description in agent file: ${filePath}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse tools (optional) - supports both comma-separated and space-separated
|
||||||
|
const toolsMatch = frontmatter.match(/tools:\s*(.+)/);
|
||||||
|
const tools = toolsMatch
|
||||||
|
? toolsMatch[1]
|
||||||
|
.split(/[,\s]+/) // Split by comma or whitespace
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter((t) => t && t !== '')
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Parse model (optional) - validate against allowed values
|
||||||
|
const modelMatch = frontmatter.match(/model:\s*(\w+)/);
|
||||||
|
const modelValue = modelMatch?.[1]?.trim();
|
||||||
|
const validModels = ['sonnet', 'opus', 'haiku', 'inherit'] as const;
|
||||||
|
const model =
|
||||||
|
modelValue && validModels.includes(modelValue as (typeof validModels)[number])
|
||||||
|
? (modelValue as 'sonnet' | 'opus' | 'haiku' | 'inherit')
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (modelValue && !model) {
|
||||||
|
logger.warn(
|
||||||
|
`Invalid model "${modelValue}" in agent file: ${filePath}. Expected one of: ${validModels.join(', ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
description,
|
||||||
|
prompt: prompt.trim(),
|
||||||
|
tools,
|
||||||
|
model,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directory entry with type information
|
||||||
|
*/
|
||||||
|
interface DirEntry {
|
||||||
|
name: string;
|
||||||
|
isFile: boolean;
|
||||||
|
isDirectory: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filesystem adapter interface for abstracting systemPaths vs secureFs
|
||||||
|
*/
|
||||||
|
interface FsAdapter {
|
||||||
|
exists: (filePath: string) => Promise<boolean>;
|
||||||
|
readdir: (dirPath: string) => Promise<DirEntry[]>;
|
||||||
|
readFile: (filePath: string) => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a filesystem adapter for system paths (user directory)
|
||||||
|
*/
|
||||||
|
function createSystemPathAdapter(): FsAdapter {
|
||||||
|
return {
|
||||||
|
exists: (filePath) => Promise.resolve(systemPaths.systemPathExists(filePath)),
|
||||||
|
readdir: async (dirPath) => {
|
||||||
|
const entryNames = await systemPaths.systemPathReaddir(dirPath);
|
||||||
|
const entries: DirEntry[] = [];
|
||||||
|
for (const name of entryNames) {
|
||||||
|
const stat = await systemPaths.systemPathStat(path.join(dirPath, name));
|
||||||
|
entries.push({
|
||||||
|
name,
|
||||||
|
isFile: stat.isFile(),
|
||||||
|
isDirectory: stat.isDirectory(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
},
|
||||||
|
readFile: (filePath) => systemPaths.systemPathReadFile(filePath, 'utf-8') as Promise<string>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a filesystem adapter for project paths (secureFs)
|
||||||
|
*/
|
||||||
|
function createSecureFsAdapter(): FsAdapter {
|
||||||
|
return {
|
||||||
|
exists: (filePath) =>
|
||||||
|
secureFs
|
||||||
|
.access(filePath)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false),
|
||||||
|
readdir: async (dirPath) => {
|
||||||
|
const entries = await secureFs.readdir(dirPath, { withFileTypes: true });
|
||||||
|
return entries.map((entry) => ({
|
||||||
|
name: entry.name,
|
||||||
|
isFile: entry.isFile(),
|
||||||
|
isDirectory: entry.isDirectory(),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
readFile: (filePath) => secureFs.readFile(filePath, 'utf-8') as Promise<string>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse agent file using the provided filesystem adapter
|
||||||
|
*/
|
||||||
|
async function parseAgentFileWithAdapter(
|
||||||
|
filePath: string,
|
||||||
|
fsAdapter: FsAdapter
|
||||||
|
): Promise<AgentDefinition | null> {
|
||||||
|
try {
|
||||||
|
const content = await fsAdapter.readFile(filePath);
|
||||||
|
return parseAgentContent(content, filePath);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to parse agent file: ${filePath}`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan a directory for agent .md files
|
||||||
|
* Agents can be in two formats:
|
||||||
|
* 1. Flat: agent-name.md (file directly in agents/)
|
||||||
|
* 2. Subdirectory: agent-name/AGENT.md (folder + file, similar to Skills)
|
||||||
|
*/
|
||||||
|
async function scanAgentsDirectory(
|
||||||
|
baseDir: string,
|
||||||
|
source: 'user' | 'project'
|
||||||
|
): Promise<FilesystemAgent[]> {
|
||||||
|
const agents: FilesystemAgent[] = [];
|
||||||
|
const fsAdapter = source === 'user' ? createSystemPathAdapter() : createSecureFsAdapter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if directory exists
|
||||||
|
const exists = await fsAdapter.exists(baseDir);
|
||||||
|
if (!exists) {
|
||||||
|
logger.debug(`Directory does not exist: ${baseDir}`);
|
||||||
|
return agents;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read all entries in the directory
|
||||||
|
const entries = await fsAdapter.readdir(baseDir);
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
// Check for flat .md file format (agent-name.md)
|
||||||
|
if (entry.isFile && entry.name.endsWith('.md')) {
|
||||||
|
const agentName = entry.name.slice(0, -3); // Remove .md extension
|
||||||
|
const agentFilePath = path.join(baseDir, entry.name);
|
||||||
|
const definition = await parseAgentFileWithAdapter(agentFilePath, fsAdapter);
|
||||||
|
if (definition) {
|
||||||
|
agents.push({
|
||||||
|
name: agentName,
|
||||||
|
definition,
|
||||||
|
source,
|
||||||
|
filePath: agentFilePath,
|
||||||
|
});
|
||||||
|
logger.debug(`Discovered ${source} agent (flat): ${agentName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check for subdirectory format (agent-name/AGENT.md)
|
||||||
|
else if (entry.isDirectory) {
|
||||||
|
const agentFilePath = path.join(baseDir, entry.name, 'AGENT.md');
|
||||||
|
const agentFileExists = await fsAdapter.exists(agentFilePath);
|
||||||
|
|
||||||
|
if (agentFileExists) {
|
||||||
|
const definition = await parseAgentFileWithAdapter(agentFilePath, fsAdapter);
|
||||||
|
if (definition) {
|
||||||
|
agents.push({
|
||||||
|
name: entry.name,
|
||||||
|
definition,
|
||||||
|
source,
|
||||||
|
filePath: agentFilePath,
|
||||||
|
});
|
||||||
|
logger.debug(`Discovered ${source} agent (subdirectory): ${entry.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to scan agents directory: ${baseDir}`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return agents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover all filesystem-based agents from user and project sources
|
||||||
|
*/
|
||||||
|
export async function discoverFilesystemAgents(
|
||||||
|
projectPath?: string,
|
||||||
|
sources: Array<'user' | 'project'> = ['user', 'project']
|
||||||
|
): Promise<FilesystemAgent[]> {
|
||||||
|
const agents: FilesystemAgent[] = [];
|
||||||
|
|
||||||
|
// Discover user-level agents from ~/.claude/agents/
|
||||||
|
if (sources.includes('user')) {
|
||||||
|
const userAgentsDir = path.join(os.homedir(), '.claude', 'agents');
|
||||||
|
const userAgents = await scanAgentsDirectory(userAgentsDir, 'user');
|
||||||
|
agents.push(...userAgents);
|
||||||
|
logger.info(`Discovered ${userAgents.length} user-level agents from ${userAgentsDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discover project-level agents from .claude/agents/
|
||||||
|
if (sources.includes('project') && projectPath) {
|
||||||
|
const projectAgentsDir = path.join(projectPath, '.claude', 'agents');
|
||||||
|
const projectAgents = await scanAgentsDirectory(projectAgentsDir, 'project');
|
||||||
|
agents.push(...projectAgents);
|
||||||
|
logger.info(`Discovered ${projectAgents.length} project-level agents from ${projectAgentsDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return agents;
|
||||||
|
}
|
||||||
@@ -241,3 +241,83 @@ export async function getPromptCustomization(
|
|||||||
enhancement: mergeEnhancementPrompts(customization.enhancement),
|
enhancement: mergeEnhancementPrompts(customization.enhancement),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Skills configuration from settings.
|
||||||
|
* Returns configuration for enabling skills and which sources to load from.
|
||||||
|
*
|
||||||
|
* @param settingsService - Settings service instance
|
||||||
|
* @returns Skills configuration with enabled state, sources, and tool inclusion flag
|
||||||
|
*/
|
||||||
|
export async function getSkillsConfiguration(settingsService: SettingsService): Promise<{
|
||||||
|
enabled: boolean;
|
||||||
|
sources: Array<'user' | 'project'>;
|
||||||
|
shouldIncludeInTools: boolean;
|
||||||
|
}> {
|
||||||
|
const settings = await settingsService.getGlobalSettings();
|
||||||
|
const enabled = settings.enableSkills ?? true; // Default enabled
|
||||||
|
const sources = settings.skillsSources ?? ['user', 'project']; // Default both sources
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
sources,
|
||||||
|
shouldIncludeInTools: enabled && sources.length > 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Subagents configuration from settings.
|
||||||
|
* Returns configuration for enabling subagents and which sources to load from.
|
||||||
|
*
|
||||||
|
* @param settingsService - Settings service instance
|
||||||
|
* @returns Subagents configuration with enabled state, sources, and tool inclusion flag
|
||||||
|
*/
|
||||||
|
export async function getSubagentsConfiguration(settingsService: SettingsService): Promise<{
|
||||||
|
enabled: boolean;
|
||||||
|
sources: Array<'user' | 'project'>;
|
||||||
|
shouldIncludeInTools: boolean;
|
||||||
|
}> {
|
||||||
|
const settings = await settingsService.getGlobalSettings();
|
||||||
|
const enabled = settings.enableSubagents ?? true; // Default enabled
|
||||||
|
const sources = settings.subagentsSources ?? ['user', 'project']; // Default both sources
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
sources,
|
||||||
|
shouldIncludeInTools: enabled && sources.length > 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get custom subagents from settings, merging global and project-level definitions.
|
||||||
|
* Project-level subagents take precedence over global ones with the same name.
|
||||||
|
*
|
||||||
|
* @param settingsService - Settings service instance
|
||||||
|
* @param projectPath - Path to the project for loading project-specific subagents
|
||||||
|
* @returns Record of agent names to definitions, or undefined if none configured
|
||||||
|
*/
|
||||||
|
export async function getCustomSubagents(
|
||||||
|
settingsService: SettingsService,
|
||||||
|
projectPath?: string
|
||||||
|
): Promise<Record<string, import('@automaker/types').AgentDefinition> | undefined> {
|
||||||
|
// Get global subagents
|
||||||
|
const globalSettings = await settingsService.getGlobalSettings();
|
||||||
|
const globalSubagents = globalSettings.customSubagents || {};
|
||||||
|
|
||||||
|
// If no project path, return only global subagents
|
||||||
|
if (!projectPath) {
|
||||||
|
return Object.keys(globalSubagents).length > 0 ? globalSubagents : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get project-specific subagents
|
||||||
|
const projectSettings = await settingsService.getProjectSettings(projectPath);
|
||||||
|
const projectSubagents = projectSettings.customSubagents || {};
|
||||||
|
|
||||||
|
// Merge: project-level takes precedence
|
||||||
|
const merged = {
|
||||||
|
...globalSubagents,
|
||||||
|
...projectSubagents,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.keys(merged).length > 0 ? merged : undefined;
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { BaseProvider } from './base-provider.js';
|
|||||||
import { classifyError, getUserFriendlyErrorMessage, createLogger } from '@automaker/utils';
|
import { classifyError, getUserFriendlyErrorMessage, createLogger } from '@automaker/utils';
|
||||||
|
|
||||||
const logger = createLogger('ClaudeProvider');
|
const logger = createLogger('ClaudeProvider');
|
||||||
import { getThinkingTokenBudget } from '@automaker/types';
|
import { getThinkingTokenBudget, validateBareModelId } from '@automaker/types';
|
||||||
import type {
|
import type {
|
||||||
ExecuteOptions,
|
ExecuteOptions,
|
||||||
ProviderMessage,
|
ProviderMessage,
|
||||||
@@ -53,6 +53,10 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
* Execute a query using Claude Agent SDK
|
* Execute a query using Claude Agent SDK
|
||||||
*/
|
*/
|
||||||
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
|
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
|
||||||
|
// Validate that model doesn't have a provider prefix
|
||||||
|
// AgentService should strip prefixes before passing to providers
|
||||||
|
validateBareModelId(options.model, 'ClaudeProvider');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
prompt,
|
prompt,
|
||||||
model,
|
model,
|
||||||
@@ -93,6 +97,8 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
...(options.mcpServers && { mcpServers: options.mcpServers }),
|
...(options.mcpServers && { mcpServers: options.mcpServers }),
|
||||||
// Extended thinking configuration
|
// Extended thinking configuration
|
||||||
...(maxThinkingTokens && { maxThinkingTokens }),
|
...(maxThinkingTokens && { maxThinkingTokens }),
|
||||||
|
// Subagents configuration for specialized task delegation
|
||||||
|
...(options.agents && { agents: options.agents }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build prompt payload
|
// Build prompt payload
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import type {
|
|||||||
import {
|
import {
|
||||||
CODEX_MODEL_MAP,
|
CODEX_MODEL_MAP,
|
||||||
supportsReasoningEffort,
|
supportsReasoningEffort,
|
||||||
|
validateBareModelId,
|
||||||
type CodexApprovalPolicy,
|
type CodexApprovalPolicy,
|
||||||
type CodexSandboxMode,
|
type CodexSandboxMode,
|
||||||
type CodexAuthStatus,
|
type CodexAuthStatus,
|
||||||
@@ -663,6 +664,10 @@ export class CodexProvider extends BaseProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
|
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
|
||||||
|
// Validate that model doesn't have a provider prefix
|
||||||
|
// AgentService should strip prefixes before passing to providers
|
||||||
|
validateBareModelId(options.model, 'CodexProvider');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mcpServers = options.mcpServers ?? {};
|
const mcpServers = options.mcpServers ?? {};
|
||||||
const hasMcpServers = Object.keys(mcpServers).length > 0;
|
const hasMcpServers = Object.keys(mcpServers).length > 0;
|
||||||
@@ -760,6 +765,7 @@ export class CodexProvider extends BaseProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Model is already bare (no prefix) - validated by executeQuery
|
||||||
const args = [
|
const args = [
|
||||||
CODEX_EXEC_SUBCOMMAND,
|
CODEX_EXEC_SUBCOMMAND,
|
||||||
CODEX_YOLO_FLAG,
|
CODEX_YOLO_FLAG,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import type {
|
|||||||
ModelDefinition,
|
ModelDefinition,
|
||||||
ContentBlock,
|
ContentBlock,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { stripProviderPrefix } from '@automaker/types';
|
import { validateBareModelId } from '@automaker/types';
|
||||||
import { validateApiKey } from '../lib/auth-utils.js';
|
import { validateApiKey } from '../lib/auth-utils.js';
|
||||||
import { getEffectivePermissions } from '../services/cursor-config-service.js';
|
import { getEffectivePermissions } from '../services/cursor-config-service.js';
|
||||||
import {
|
import {
|
||||||
@@ -317,8 +317,8 @@ export class CursorProvider extends CliProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildCliArgs(options: ExecuteOptions): string[] {
|
buildCliArgs(options: ExecuteOptions): string[] {
|
||||||
// Extract model (strip 'cursor-' prefix if present)
|
// Model is already bare (no prefix) - validated by executeQuery
|
||||||
const model = stripProviderPrefix(options.model || 'auto');
|
const model = options.model || 'auto';
|
||||||
|
|
||||||
// Build CLI arguments for cursor-agent
|
// Build CLI arguments for cursor-agent
|
||||||
// NOTE: Prompt is NOT included here - it's passed via stdin to avoid
|
// NOTE: Prompt is NOT included here - it's passed via stdin to avoid
|
||||||
@@ -649,6 +649,10 @@ export class CursorProvider extends CliProvider {
|
|||||||
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
|
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
|
||||||
this.ensureCliDetected();
|
this.ensureCliDetected();
|
||||||
|
|
||||||
|
// Validate that model doesn't have a provider prefix
|
||||||
|
// AgentService should strip prefixes before passing to providers
|
||||||
|
validateBareModelId(options.model, 'CursorProvider');
|
||||||
|
|
||||||
if (!this.cliPath) {
|
if (!this.cliPath) {
|
||||||
throw this.createError(
|
throw this.createError(
|
||||||
CursorErrorCode.NOT_INSTALLED,
|
CursorErrorCode.NOT_INSTALLED,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { createGetProjectHandler } from './routes/get-project.js';
|
|||||||
import { createUpdateProjectHandler } from './routes/update-project.js';
|
import { createUpdateProjectHandler } from './routes/update-project.js';
|
||||||
import { createMigrateHandler } from './routes/migrate.js';
|
import { createMigrateHandler } from './routes/migrate.js';
|
||||||
import { createStatusHandler } from './routes/status.js';
|
import { createStatusHandler } from './routes/status.js';
|
||||||
|
import { createDiscoverAgentsHandler } from './routes/discover-agents.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create settings router with all endpoints
|
* Create settings router with all endpoints
|
||||||
@@ -39,6 +40,7 @@ import { createStatusHandler } from './routes/status.js';
|
|||||||
* - POST /project - Get project settings (requires projectPath in body)
|
* - POST /project - Get project settings (requires projectPath in body)
|
||||||
* - PUT /project - Update project settings
|
* - PUT /project - Update project settings
|
||||||
* - POST /migrate - Migrate settings from localStorage
|
* - POST /migrate - Migrate settings from localStorage
|
||||||
|
* - POST /agents/discover - Discover filesystem agents from .claude/agents/ (read-only)
|
||||||
*
|
*
|
||||||
* @param settingsService - Instance of SettingsService for file I/O
|
* @param settingsService - Instance of SettingsService for file I/O
|
||||||
* @returns Express Router configured with all settings endpoints
|
* @returns Express Router configured with all settings endpoints
|
||||||
@@ -72,5 +74,8 @@ export function createSettingsRoutes(settingsService: SettingsService): Router {
|
|||||||
// Migration from localStorage
|
// Migration from localStorage
|
||||||
router.post('/migrate', createMigrateHandler(settingsService));
|
router.post('/migrate', createMigrateHandler(settingsService));
|
||||||
|
|
||||||
|
// Filesystem agents discovery (read-only)
|
||||||
|
router.post('/agents/discover', createDiscoverAgentsHandler());
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
61
apps/server/src/routes/settings/routes/discover-agents.ts
Normal file
61
apps/server/src/routes/settings/routes/discover-agents.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Discover Agents Route - Returns filesystem-based agents from .claude/agents/
|
||||||
|
*
|
||||||
|
* Scans both user-level (~/.claude/agents/) and project-level (.claude/agents/)
|
||||||
|
* directories for AGENT.md files and returns parsed agent definitions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Request, Response } from 'express';
|
||||||
|
import { discoverFilesystemAgents } from '../../../lib/agent-discovery.js';
|
||||||
|
import { createLogger } from '@automaker/utils';
|
||||||
|
|
||||||
|
const logger = createLogger('DiscoverAgentsRoute');
|
||||||
|
|
||||||
|
interface DiscoverAgentsRequest {
|
||||||
|
projectPath?: string;
|
||||||
|
sources?: Array<'user' | 'project'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create handler for discovering filesystem agents
|
||||||
|
*
|
||||||
|
* POST /api/settings/agents/discover
|
||||||
|
* Body: { projectPath?: string, sources?: ['user', 'project'] }
|
||||||
|
*
|
||||||
|
* Returns:
|
||||||
|
* {
|
||||||
|
* success: true,
|
||||||
|
* agents: Array<{
|
||||||
|
* name: string,
|
||||||
|
* definition: AgentDefinition,
|
||||||
|
* source: 'user' | 'project',
|
||||||
|
* filePath: string
|
||||||
|
* }>
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export function createDiscoverAgentsHandler() {
|
||||||
|
return async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { projectPath, sources = ['user', 'project'] } = req.body as DiscoverAgentsRequest;
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Discovering agents from sources: ${sources.join(', ')}${projectPath ? ` (project: ${projectPath})` : ''}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const agents = await discoverFilesystemAgents(projectPath, sources);
|
||||||
|
|
||||||
|
logger.info(`Discovered ${agents.length} filesystem agents`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
agents,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to discover agents:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to discover agents',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import path from 'path';
|
|||||||
import * as secureFs from '../lib/secure-fs.js';
|
import * as secureFs from '../lib/secure-fs.js';
|
||||||
import type { EventEmitter } from '../lib/events.js';
|
import type { EventEmitter } from '../lib/events.js';
|
||||||
import type { ExecuteOptions, ThinkingLevel, ReasoningEffort } from '@automaker/types';
|
import type { ExecuteOptions, ThinkingLevel, ReasoningEffort } from '@automaker/types';
|
||||||
|
import { stripProviderPrefix } from '@automaker/types';
|
||||||
import {
|
import {
|
||||||
readImageAsBase64,
|
readImageAsBase64,
|
||||||
buildPromptWithImages,
|
buildPromptWithImages,
|
||||||
@@ -25,6 +26,9 @@ import {
|
|||||||
filterClaudeMdFromContext,
|
filterClaudeMdFromContext,
|
||||||
getMCPServersFromSettings,
|
getMCPServersFromSettings,
|
||||||
getPromptCustomization,
|
getPromptCustomization,
|
||||||
|
getSkillsConfiguration,
|
||||||
|
getSubagentsConfiguration,
|
||||||
|
getCustomSubagents,
|
||||||
} from '../lib/settings-helpers.js';
|
} from '../lib/settings-helpers.js';
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
@@ -254,6 +258,22 @@ export class AgentService {
|
|||||||
// Load MCP servers from settings (global setting only)
|
// Load MCP servers from settings (global setting only)
|
||||||
const mcpServers = await getMCPServersFromSettings(this.settingsService, '[AgentService]');
|
const mcpServers = await getMCPServersFromSettings(this.settingsService, '[AgentService]');
|
||||||
|
|
||||||
|
// Get Skills configuration from settings
|
||||||
|
const skillsConfig = this.settingsService
|
||||||
|
? await getSkillsConfiguration(this.settingsService)
|
||||||
|
: { enabled: false, sources: [] as Array<'user' | 'project'>, shouldIncludeInTools: false };
|
||||||
|
|
||||||
|
// Get Subagents configuration from settings
|
||||||
|
const subagentsConfig = this.settingsService
|
||||||
|
? await getSubagentsConfiguration(this.settingsService)
|
||||||
|
: { enabled: false, sources: [] as Array<'user' | 'project'>, shouldIncludeInTools: false };
|
||||||
|
|
||||||
|
// Get custom subagents from settings (merge global + project-level) only if enabled
|
||||||
|
const customSubagents =
|
||||||
|
this.settingsService && subagentsConfig.enabled
|
||||||
|
? await getCustomSubagents(this.settingsService, effectiveWorkDir)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.)
|
// Load project context files (CLAUDE.md, CODE_QUALITY.md, etc.)
|
||||||
const contextResult = await loadContextFiles({
|
const contextResult = await loadContextFiles({
|
||||||
projectPath: effectiveWorkDir,
|
projectPath: effectiveWorkDir,
|
||||||
@@ -288,24 +308,69 @@ export class AgentService {
|
|||||||
// Extract model, maxTurns, and allowedTools from SDK options
|
// Extract model, maxTurns, and allowedTools from SDK options
|
||||||
const effectiveModel = sdkOptions.model!;
|
const effectiveModel = sdkOptions.model!;
|
||||||
const maxTurns = sdkOptions.maxTurns;
|
const maxTurns = sdkOptions.maxTurns;
|
||||||
const allowedTools = sdkOptions.allowedTools as string[] | undefined;
|
let allowedTools = sdkOptions.allowedTools as string[] | undefined;
|
||||||
|
|
||||||
// Get provider for this model
|
// Build merged settingSources array using Set for automatic deduplication
|
||||||
|
const sdkSettingSources = (sdkOptions.settingSources ?? []).filter(
|
||||||
|
(source): source is 'user' | 'project' => source === 'user' || source === 'project'
|
||||||
|
);
|
||||||
|
const skillSettingSources = skillsConfig.enabled ? skillsConfig.sources : [];
|
||||||
|
const settingSources = [...new Set([...sdkSettingSources, ...skillSettingSources])];
|
||||||
|
|
||||||
|
// Enhance allowedTools with Skills and Subagents tools
|
||||||
|
// These tools are not in the provider's default set - they're added dynamically based on settings
|
||||||
|
const needsSkillTool = skillsConfig.shouldIncludeInTools;
|
||||||
|
const needsTaskTool =
|
||||||
|
subagentsConfig.shouldIncludeInTools &&
|
||||||
|
customSubagents &&
|
||||||
|
Object.keys(customSubagents).length > 0;
|
||||||
|
|
||||||
|
// Base tools that match the provider's default set
|
||||||
|
const baseTools = ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'];
|
||||||
|
|
||||||
|
if (allowedTools) {
|
||||||
|
allowedTools = [...allowedTools]; // Create a copy to avoid mutating SDK options
|
||||||
|
// Add Skill tool if skills are enabled
|
||||||
|
if (needsSkillTool && !allowedTools.includes('Skill')) {
|
||||||
|
allowedTools.push('Skill');
|
||||||
|
}
|
||||||
|
// Add Task tool if custom subagents are configured
|
||||||
|
if (needsTaskTool && !allowedTools.includes('Task')) {
|
||||||
|
allowedTools.push('Task');
|
||||||
|
}
|
||||||
|
} else if (needsSkillTool || needsTaskTool) {
|
||||||
|
// If no allowedTools specified but we need to add Skill/Task tools,
|
||||||
|
// build the full list including base tools
|
||||||
|
allowedTools = [...baseTools];
|
||||||
|
if (needsSkillTool) {
|
||||||
|
allowedTools.push('Skill');
|
||||||
|
}
|
||||||
|
if (needsTaskTool) {
|
||||||
|
allowedTools.push('Task');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get provider for this model (with prefix)
|
||||||
const provider = ProviderFactory.getProviderForModel(effectiveModel);
|
const provider = ProviderFactory.getProviderForModel(effectiveModel);
|
||||||
|
|
||||||
|
// Strip provider prefix - providers should receive bare model IDs
|
||||||
|
const bareModel = stripProviderPrefix(effectiveModel);
|
||||||
|
|
||||||
// Build options for provider
|
// Build options for provider
|
||||||
const options: ExecuteOptions = {
|
const options: ExecuteOptions = {
|
||||||
prompt: '', // Will be set below based on images
|
prompt: '', // Will be set below based on images
|
||||||
model: effectiveModel,
|
model: bareModel, // Bare model ID (e.g., "gpt-5.1-codex-max", "composer-1")
|
||||||
|
originalModel: effectiveModel, // Original with prefix for logging (e.g., "codex-gpt-5.1-codex-max")
|
||||||
cwd: effectiveWorkDir,
|
cwd: effectiveWorkDir,
|
||||||
systemPrompt: sdkOptions.systemPrompt,
|
systemPrompt: sdkOptions.systemPrompt,
|
||||||
maxTurns: maxTurns,
|
maxTurns: maxTurns,
|
||||||
allowedTools: allowedTools,
|
allowedTools: allowedTools,
|
||||||
abortController: session.abortController!,
|
abortController: session.abortController!,
|
||||||
conversationHistory: conversationHistory.length > 0 ? conversationHistory : undefined,
|
conversationHistory: conversationHistory.length > 0 ? conversationHistory : undefined,
|
||||||
settingSources: sdkOptions.settingSources,
|
settingSources: settingSources.length > 0 ? settingSources : undefined,
|
||||||
sdkSessionId: session.sdkSessionId, // Pass SDK session ID for resuming
|
sdkSessionId: session.sdkSessionId, // Pass SDK session ID for resuming
|
||||||
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, // Pass MCP servers configuration
|
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, // Pass MCP servers configuration
|
||||||
|
agents: customSubagents, // Pass custom subagents for task delegation
|
||||||
thinkingLevel: effectiveThinkingLevel, // Pass thinking level for Claude models
|
thinkingLevel: effectiveThinkingLevel, // Pass thinking level for Claude models
|
||||||
reasoningEffort: effectiveReasoningEffort, // Pass reasoning effort for Codex models
|
reasoningEffort: effectiveReasoningEffort, // Pass reasoning effort for Codex models
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import type { SettingsService } from './settings-service.js';
|
|||||||
import type { FeatureLoader } from './feature-loader.js';
|
import type { FeatureLoader } from './feature-loader.js';
|
||||||
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
|
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
|
||||||
import { resolveModelString } from '@automaker/model-resolver';
|
import { resolveModelString } from '@automaker/model-resolver';
|
||||||
|
import { stripProviderPrefix } from '@automaker/types';
|
||||||
|
|
||||||
const logger = createLogger('IdeationService');
|
const logger = createLogger('IdeationService');
|
||||||
|
|
||||||
@@ -201,7 +202,7 @@ export class IdeationService {
|
|||||||
existingWorkContext
|
existingWorkContext
|
||||||
);
|
);
|
||||||
|
|
||||||
// Resolve model alias to canonical identifier
|
// Resolve model alias to canonical identifier (with prefix)
|
||||||
const modelId = resolveModelString(options?.model ?? 'sonnet');
|
const modelId = resolveModelString(options?.model ?? 'sonnet');
|
||||||
|
|
||||||
// Create SDK options
|
// Create SDK options
|
||||||
@@ -214,9 +215,13 @@ export class IdeationService {
|
|||||||
|
|
||||||
const provider = ProviderFactory.getProviderForModel(modelId);
|
const provider = ProviderFactory.getProviderForModel(modelId);
|
||||||
|
|
||||||
|
// Strip provider prefix - providers need bare model IDs
|
||||||
|
const bareModel = stripProviderPrefix(modelId);
|
||||||
|
|
||||||
const executeOptions: ExecuteOptions = {
|
const executeOptions: ExecuteOptions = {
|
||||||
prompt: message,
|
prompt: message,
|
||||||
model: modelId,
|
model: bareModel,
|
||||||
|
originalModel: modelId,
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
systemPrompt: sdkOptions.systemPrompt,
|
systemPrompt: sdkOptions.systemPrompt,
|
||||||
maxTurns: 1, // Single turn for ideation
|
maxTurns: 1, // Single turn for ideation
|
||||||
@@ -648,7 +653,7 @@ export class IdeationService {
|
|||||||
existingWorkContext
|
existingWorkContext
|
||||||
);
|
);
|
||||||
|
|
||||||
// Resolve model alias to canonical identifier
|
// Resolve model alias to canonical identifier (with prefix)
|
||||||
const modelId = resolveModelString('sonnet');
|
const modelId = resolveModelString('sonnet');
|
||||||
|
|
||||||
// Create SDK options
|
// Create SDK options
|
||||||
@@ -661,9 +666,13 @@ export class IdeationService {
|
|||||||
|
|
||||||
const provider = ProviderFactory.getProviderForModel(modelId);
|
const provider = ProviderFactory.getProviderForModel(modelId);
|
||||||
|
|
||||||
|
// Strip provider prefix - providers need bare model IDs
|
||||||
|
const bareModel = stripProviderPrefix(modelId);
|
||||||
|
|
||||||
const executeOptions: ExecuteOptions = {
|
const executeOptions: ExecuteOptions = {
|
||||||
prompt: prompt.prompt,
|
prompt: prompt.prompt,
|
||||||
model: modelId,
|
model: bareModel,
|
||||||
|
originalModel: modelId,
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
systemPrompt: sdkOptions.systemPrompt,
|
systemPrompt: sdkOptions.systemPrompt,
|
||||||
maxTurns: 1,
|
maxTurns: 1,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ describe('claude-provider.ts', () => {
|
|||||||
|
|
||||||
const generator = provider.executeQuery({
|
const generator = provider.executeQuery({
|
||||||
prompt: 'Hello',
|
prompt: 'Hello',
|
||||||
|
model: 'claude-opus-4-5-20251101',
|
||||||
cwd: '/test',
|
cwd: '/test',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,6 +89,7 @@ describe('claude-provider.ts', () => {
|
|||||||
|
|
||||||
const generator = provider.executeQuery({
|
const generator = provider.executeQuery({
|
||||||
prompt: 'Test',
|
prompt: 'Test',
|
||||||
|
model: 'claude-opus-4-5-20251101',
|
||||||
cwd: '/test',
|
cwd: '/test',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -112,6 +114,7 @@ describe('claude-provider.ts', () => {
|
|||||||
|
|
||||||
const generator = provider.executeQuery({
|
const generator = provider.executeQuery({
|
||||||
prompt: 'Test',
|
prompt: 'Test',
|
||||||
|
model: 'claude-opus-4-5-20251101',
|
||||||
cwd: '/test',
|
cwd: '/test',
|
||||||
abortController,
|
abortController,
|
||||||
});
|
});
|
||||||
@@ -140,6 +143,7 @@ describe('claude-provider.ts', () => {
|
|||||||
|
|
||||||
const generator = provider.executeQuery({
|
const generator = provider.executeQuery({
|
||||||
prompt: 'Current message',
|
prompt: 'Current message',
|
||||||
|
model: 'claude-opus-4-5-20251101',
|
||||||
cwd: '/test',
|
cwd: '/test',
|
||||||
conversationHistory,
|
conversationHistory,
|
||||||
sdkSessionId: 'test-session-id',
|
sdkSessionId: 'test-session-id',
|
||||||
@@ -170,6 +174,7 @@ describe('claude-provider.ts', () => {
|
|||||||
|
|
||||||
const generator = provider.executeQuery({
|
const generator = provider.executeQuery({
|
||||||
prompt: arrayPrompt as any,
|
prompt: arrayPrompt as any,
|
||||||
|
model: 'claude-opus-4-5-20251101',
|
||||||
cwd: '/test',
|
cwd: '/test',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -189,6 +194,7 @@ describe('claude-provider.ts', () => {
|
|||||||
|
|
||||||
const generator = provider.executeQuery({
|
const generator = provider.executeQuery({
|
||||||
prompt: 'Test',
|
prompt: 'Test',
|
||||||
|
model: 'claude-opus-4-5-20251101',
|
||||||
cwd: '/test',
|
cwd: '/test',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -214,6 +220,7 @@ describe('claude-provider.ts', () => {
|
|||||||
|
|
||||||
const generator = provider.executeQuery({
|
const generator = provider.executeQuery({
|
||||||
prompt: 'Test',
|
prompt: 'Test',
|
||||||
|
model: 'claude-opus-4-5-20251101',
|
||||||
cwd: '/test',
|
cwd: '/test',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { useCliStatus } from '../hooks/use-cli-status';
|
|||||||
import { ClaudeCliStatus } from '../cli-status/claude-cli-status';
|
import { ClaudeCliStatus } from '../cli-status/claude-cli-status';
|
||||||
import { ClaudeMdSettings } from '../claude/claude-md-settings';
|
import { ClaudeMdSettings } from '../claude/claude-md-settings';
|
||||||
import { ClaudeUsageSection } from '../api-keys/claude-usage-section';
|
import { ClaudeUsageSection } from '../api-keys/claude-usage-section';
|
||||||
|
import { SkillsSection } from './claude-settings-tab/skills-section';
|
||||||
|
import { SubagentsSection } from './claude-settings-tab/subagents-section';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
export function ClaudeSettingsTab() {
|
export function ClaudeSettingsTab() {
|
||||||
@@ -43,6 +45,13 @@ export function ClaudeSettingsTab() {
|
|||||||
autoLoadClaudeMd={autoLoadClaudeMd}
|
autoLoadClaudeMd={autoLoadClaudeMd}
|
||||||
onAutoLoadClaudeMdChange={setAutoLoadClaudeMd}
|
onAutoLoadClaudeMdChange={setAutoLoadClaudeMd}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Skills Configuration */}
|
||||||
|
<SkillsSection />
|
||||||
|
|
||||||
|
{/* Custom Subagents */}
|
||||||
|
<SubagentsSection />
|
||||||
|
|
||||||
{showUsageTracking && <ClaudeUsageSection />}
|
{showUsageTracking && <ClaudeUsageSection />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Hooks barrel export for Claude Settings Tab
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { useSkillsSettings } from './use-skills-settings';
|
||||||
|
export { useSubagents } from './use-subagents';
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* Skills Settings Hook - Manages Skills configuration state
|
||||||
|
*
|
||||||
|
* Provides state management for enabling/disabling Skills and
|
||||||
|
* configuring which sources to load Skills from (user/project).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
|
|
||||||
|
export function useSkillsSettings() {
|
||||||
|
const enabled = useAppStore((state) => state.enableSkills);
|
||||||
|
const sources = useAppStore((state) => state.skillsSources);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const updateEnabled = async (newEnabled: boolean) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api.settings) {
|
||||||
|
throw new Error('Settings API not available');
|
||||||
|
}
|
||||||
|
await api.settings.updateGlobal({ enableSkills: newEnabled });
|
||||||
|
// Update local store after successful server update
|
||||||
|
useAppStore.setState({ enableSkills: newEnabled });
|
||||||
|
toast.success(newEnabled ? 'Skills enabled' : 'Skills disabled');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to update skills settings');
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSources = async (newSources: Array<'user' | 'project'>) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api.settings) {
|
||||||
|
throw new Error('Settings API not available');
|
||||||
|
}
|
||||||
|
await api.settings.updateGlobal({ skillsSources: newSources });
|
||||||
|
// Update local store after successful server update
|
||||||
|
useAppStore.setState({ skillsSources: newSources });
|
||||||
|
toast.success('Skills sources updated');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to update skills sources');
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
sources,
|
||||||
|
updateEnabled,
|
||||||
|
updateSources,
|
||||||
|
isLoading,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* Subagents Settings Hook - Manages Subagents configuration state
|
||||||
|
*
|
||||||
|
* Provides state management for enabling/disabling Subagents and
|
||||||
|
* configuring which sources to load Subagents from (user/project).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
|
|
||||||
|
export function useSubagentsSettings() {
|
||||||
|
const enabled = useAppStore((state) => state.enableSubagents);
|
||||||
|
const sources = useAppStore((state) => state.subagentsSources);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const updateEnabled = async (newEnabled: boolean) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api.settings) {
|
||||||
|
throw new Error('Settings API not available');
|
||||||
|
}
|
||||||
|
await api.settings.updateGlobal({ enableSubagents: newEnabled });
|
||||||
|
// Update local store after successful server update
|
||||||
|
useAppStore.setState({ enableSubagents: newEnabled });
|
||||||
|
toast.success(newEnabled ? 'Subagents enabled' : 'Subagents disabled');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to update subagents settings');
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSources = async (newSources: Array<'user' | 'project'>) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api.settings) {
|
||||||
|
throw new Error('Settings API not available');
|
||||||
|
}
|
||||||
|
await api.settings.updateGlobal({ subagentsSources: newSources });
|
||||||
|
// Update local store after successful server update
|
||||||
|
useAppStore.setState({ subagentsSources: newSources });
|
||||||
|
toast.success('Subagents sources updated');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to update subagents sources');
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
sources,
|
||||||
|
updateEnabled,
|
||||||
|
updateSources,
|
||||||
|
isLoading,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* Subagents Hook - Manages custom subagent definitions
|
||||||
|
*
|
||||||
|
* Provides read-only view of custom subagent configurations
|
||||||
|
* used for specialized task delegation. Supports:
|
||||||
|
* - Filesystem agents (AGENT.md files in .claude/agents/) - user and project-level (read-only)
|
||||||
|
*
|
||||||
|
* Filesystem agents are discovered via the server API and displayed in the UI.
|
||||||
|
* Agent definitions in settings JSON are used server-side only.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import type { AgentDefinition } from '@automaker/types';
|
||||||
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
|
|
||||||
|
export type SubagentScope = 'global' | 'project';
|
||||||
|
export type SubagentType = 'filesystem';
|
||||||
|
export type FilesystemSource = 'user' | 'project';
|
||||||
|
|
||||||
|
export interface SubagentWithScope {
|
||||||
|
name: string;
|
||||||
|
definition: AgentDefinition;
|
||||||
|
scope: SubagentScope;
|
||||||
|
type: SubagentType;
|
||||||
|
source: FilesystemSource;
|
||||||
|
filePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilesystemAgent {
|
||||||
|
name: string;
|
||||||
|
definition: AgentDefinition;
|
||||||
|
source: FilesystemSource;
|
||||||
|
filePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSubagents() {
|
||||||
|
const currentProject = useAppStore((state) => state.currentProject);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [subagentsWithScope, setSubagentsWithScope] = useState<SubagentWithScope[]>([]);
|
||||||
|
|
||||||
|
// Fetch filesystem agents
|
||||||
|
const fetchFilesystemAgents = useCallback(async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api.settings) {
|
||||||
|
console.warn('Settings API not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await api.settings.discoverAgents(currentProject?.path, ['user', 'project']);
|
||||||
|
|
||||||
|
if (data.success && data.agents) {
|
||||||
|
// Transform filesystem agents to SubagentWithScope format
|
||||||
|
const agents: SubagentWithScope[] = data.agents.map(
|
||||||
|
({ name, definition, source, filePath }: FilesystemAgent) => ({
|
||||||
|
name,
|
||||||
|
definition,
|
||||||
|
scope: source === 'user' ? 'global' : 'project',
|
||||||
|
type: 'filesystem' as const,
|
||||||
|
source,
|
||||||
|
filePath,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setSubagentsWithScope(agents);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch filesystem agents:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [currentProject?.path]);
|
||||||
|
|
||||||
|
// Fetch filesystem agents on mount and when project changes
|
||||||
|
useEffect(() => {
|
||||||
|
fetchFilesystemAgents();
|
||||||
|
}, [fetchFilesystemAgents]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subagentsWithScope,
|
||||||
|
isLoading,
|
||||||
|
hasProject: !!currentProject,
|
||||||
|
refreshFilesystemAgents: fetchFilesystemAgents,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Claude Settings Tab components barrel export
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { SkillsSection } from './skills-section';
|
||||||
|
export { SubagentsSection } from './subagents-section';
|
||||||
|
export { SubagentCard } from './subagent-card';
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
/**
|
||||||
|
* Skills Section - UI for managing Skills configuration
|
||||||
|
*
|
||||||
|
* Allows users to enable/disable Skills and select which directories
|
||||||
|
* to load Skills from (user ~/.claude/skills/ or project .claude/skills/).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Zap, Globe, FolderOpen, ExternalLink, Sparkles } from 'lucide-react';
|
||||||
|
import { useSkillsSettings } from './hooks/use-skills-settings';
|
||||||
|
|
||||||
|
export function SkillsSection() {
|
||||||
|
const { enabled, sources, updateEnabled, updateSources, isLoading } = useSkillsSettings();
|
||||||
|
|
||||||
|
const toggleSource = (source: 'user' | 'project') => {
|
||||||
|
if (sources.includes(source)) {
|
||||||
|
updateSources(sources.filter((s: 'user' | 'project') => s !== source));
|
||||||
|
} else {
|
||||||
|
updateSources([...sources, source]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-2xl overflow-hidden',
|
||||||
|
'border border-border/50',
|
||||||
|
'bg-linear-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||||
|
'shadow-sm shadow-black/5'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-border/30">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-amber-500/20 flex items-center justify-center">
|
||||||
|
<Zap className="w-5 h-5 text-amber-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-base flex items-center gap-2">
|
||||||
|
Skills
|
||||||
|
{enabled && (
|
||||||
|
<span className="text-xs font-normal px-2 py-0.5 rounded-full bg-amber-500/20 text-amber-500">
|
||||||
|
{sources.length} source{sources.length !== 1 ? 's' : ''} active
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mt-0.5">
|
||||||
|
Filesystem-based capabilities Claude invokes autonomously
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="enable-skills"
|
||||||
|
checked={enabled}
|
||||||
|
onCheckedChange={updateEnabled}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
{/* Sources Selection */}
|
||||||
|
{enabled && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
Load Skills from
|
||||||
|
</Label>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
{/* User Skills Option */}
|
||||||
|
<label
|
||||||
|
htmlFor="source-user"
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-3 p-3 rounded-xl border cursor-pointer transition-all duration-200',
|
||||||
|
sources.includes('user')
|
||||||
|
? 'border-amber-500/50 bg-amber-500/10'
|
||||||
|
: 'border-border/50 bg-accent/20 hover:bg-accent/30'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id="source-user"
|
||||||
|
checked={sources.includes('user')}
|
||||||
|
onCheckedChange={() => toggleSource('user')}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="data-[state=checked]:bg-amber-500 data-[state=checked]:border-amber-500"
|
||||||
|
/>
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-muted/50 flex items-center justify-center shrink-0">
|
||||||
|
<Globe className="w-4 h-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<span className="text-sm font-medium">User Skills</span>
|
||||||
|
<span className="block text-xs text-muted-foreground mt-0.5 truncate">
|
||||||
|
~/.claude/skills/ — Available across all projects
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Project Skills Option */}
|
||||||
|
<label
|
||||||
|
htmlFor="source-project"
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-3 p-3 rounded-xl border cursor-pointer transition-all duration-200',
|
||||||
|
sources.includes('project')
|
||||||
|
? 'border-amber-500/50 bg-amber-500/10'
|
||||||
|
: 'border-border/50 bg-accent/20 hover:bg-accent/30'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id="source-project"
|
||||||
|
checked={sources.includes('project')}
|
||||||
|
onCheckedChange={() => toggleSource('project')}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="data-[state=checked]:bg-amber-500 data-[state=checked]:border-amber-500"
|
||||||
|
/>
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-muted/50 flex items-center justify-center shrink-0">
|
||||||
|
<FolderOpen className="w-4 h-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<span className="text-sm font-medium">Project Skills</span>
|
||||||
|
<span className="block text-xs text-muted-foreground mt-0.5 truncate">
|
||||||
|
.claude/skills/ — Version-controlled with project
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Help Text */}
|
||||||
|
{enabled && (
|
||||||
|
<div className="rounded-xl border border-border/30 bg-muted/30 p-4 space-y-3">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="w-6 h-6 rounded-md bg-brand-500/20 flex items-center justify-center shrink-0 mt-0.5">
|
||||||
|
<Sparkles className="w-3.5 h-3.5 text-brand-500" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground space-y-1">
|
||||||
|
<p className="font-medium text-foreground/80">Auto-Discovery</p>
|
||||||
|
<p>
|
||||||
|
Skills are automatically discovered when agents start. Define skills as{' '}
|
||||||
|
<code className="text-xs bg-muted px-1 rounded">SKILL.md</code> files.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="https://code.claude.com/docs/en/skills"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 text-xs text-brand-500 hover:text-brand-400 transition-colors"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
|
View Skills documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Disabled State Empty Message */}
|
||||||
|
{!enabled && (
|
||||||
|
<div className="text-center py-6 text-muted-foreground">
|
||||||
|
<Zap className="w-10 h-10 mx-auto mb-3 opacity-30" />
|
||||||
|
<p className="text-sm">Skills are disabled</p>
|
||||||
|
<p className="text-xs mt-1">Enable to load filesystem-based capabilities</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* Subagent Card - Display card for a single subagent definition
|
||||||
|
*
|
||||||
|
* Shows the subagent's name, description, model, tool count, scope, and type.
|
||||||
|
* Read-only view - agents are managed by editing .md files directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Markdown } from '@/components/ui/markdown';
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
Globe,
|
||||||
|
FolderOpen,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
Bot,
|
||||||
|
Cpu,
|
||||||
|
Wrench,
|
||||||
|
FileCode,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import type { SubagentWithScope } from './hooks/use-subagents';
|
||||||
|
|
||||||
|
interface SubagentCardProps {
|
||||||
|
agent: SubagentWithScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SubagentCard({ agent }: SubagentCardProps) {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const { name, definition, scope, filePath } = agent;
|
||||||
|
|
||||||
|
const toolCount = definition.tools?.length ?? 'all';
|
||||||
|
const modelDisplay =
|
||||||
|
definition.model === 'inherit' || !definition.model
|
||||||
|
? 'Inherit'
|
||||||
|
: definition.model.charAt(0).toUpperCase() + definition.model.slice(1);
|
||||||
|
|
||||||
|
// Scope icon and label
|
||||||
|
const ScopeIcon = scope === 'global' ? Globe : FolderOpen;
|
||||||
|
const scopeLabel = scope === 'global' ? 'User' : 'Project';
|
||||||
|
|
||||||
|
// Model color based on type
|
||||||
|
const getModelColor = () => {
|
||||||
|
const model = definition.model?.toLowerCase();
|
||||||
|
if (model === 'opus') return 'text-violet-500 bg-violet-500/10 border-violet-500/30';
|
||||||
|
if (model === 'sonnet') return 'text-blue-500 bg-blue-500/10 border-blue-500/30';
|
||||||
|
if (model === 'haiku') return 'text-emerald-500 bg-emerald-500/10 border-emerald-500/30';
|
||||||
|
return 'text-muted-foreground bg-muted/50 border-border/50';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-xl border transition-all duration-200',
|
||||||
|
'border-border/50 bg-accent/20',
|
||||||
|
'hover:bg-accent/30 hover:border-border/70'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Main Card Content */}
|
||||||
|
<div className="flex items-start gap-3 p-4">
|
||||||
|
{/* Agent Icon */}
|
||||||
|
<div className="w-9 h-9 rounded-lg bg-violet-500/15 flex items-center justify-center shrink-0 mt-0.5">
|
||||||
|
<Bot className="w-4 h-4 text-violet-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
{/* Header Row */}
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<h4 className="font-medium text-sm">{name}</h4>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className={cn('flex items-center gap-1', getModelColor())}
|
||||||
|
>
|
||||||
|
<Cpu className="h-3 w-3" />
|
||||||
|
{modelDisplay}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="muted" size="sm" className="flex items-center gap-1">
|
||||||
|
<Wrench className="h-3 w-3" />
|
||||||
|
{toolCount === 'all' ? 'All' : toolCount} tools
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="muted" size="sm" className="flex items-center gap-1">
|
||||||
|
<ScopeIcon className="h-3 w-3" />
|
||||||
|
{scopeLabel}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-sm text-muted-foreground mt-1.5 line-clamp-2">
|
||||||
|
{definition.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* File Path */}
|
||||||
|
{filePath && (
|
||||||
|
<div className="flex items-center gap-1.5 mt-2 text-xs text-muted-foreground/60">
|
||||||
|
<FileCode className="h-3 w-3" />
|
||||||
|
<span className="font-mono truncate">{filePath}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expand Button */}
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
'p-1.5 rounded-md transition-colors shrink-0',
|
||||||
|
'hover:bg-muted/50 text-muted-foreground hover:text-foreground',
|
||||||
|
'cursor-pointer'
|
||||||
|
)}
|
||||||
|
title={isExpanded ? 'Hide prompt' : 'View prompt'}
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expandable Prompt Section */}
|
||||||
|
<CollapsibleContent>
|
||||||
|
<div className="px-4 pb-4 pt-0">
|
||||||
|
<div className="ml-12 rounded-lg border border-border/30 bg-muted/30 p-4 overflow-auto max-h-64">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground mb-2 uppercase tracking-wide">
|
||||||
|
System Prompt
|
||||||
|
</div>
|
||||||
|
<Markdown className="text-xs prose-sm">{definition.prompt}</Markdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
/**
|
||||||
|
* Subagents Section - UI for managing Subagents configuration
|
||||||
|
*
|
||||||
|
* Allows users to enable/disable Subagents and select which directories
|
||||||
|
* to load Subagents from (user ~/.claude/agents/ or project .claude/agents/).
|
||||||
|
*
|
||||||
|
* Displays agents discovered from:
|
||||||
|
* - User-level: ~/.claude/agents/
|
||||||
|
* - Project-level: .claude/agents/
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
Bot,
|
||||||
|
RefreshCw,
|
||||||
|
Loader2,
|
||||||
|
Users,
|
||||||
|
ExternalLink,
|
||||||
|
Globe,
|
||||||
|
FolderOpen,
|
||||||
|
Sparkles,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useSubagents } from './hooks/use-subagents';
|
||||||
|
import { useSubagentsSettings } from './hooks/use-subagents-settings';
|
||||||
|
import { SubagentCard } from './subagent-card';
|
||||||
|
|
||||||
|
export function SubagentsSection() {
|
||||||
|
const {
|
||||||
|
subagentsWithScope,
|
||||||
|
isLoading: isLoadingAgents,
|
||||||
|
hasProject,
|
||||||
|
refreshFilesystemAgents,
|
||||||
|
} = useSubagents();
|
||||||
|
const {
|
||||||
|
enabled,
|
||||||
|
sources,
|
||||||
|
updateEnabled,
|
||||||
|
updateSources,
|
||||||
|
isLoading: isLoadingSettings,
|
||||||
|
} = useSubagentsSettings();
|
||||||
|
|
||||||
|
const isLoading = isLoadingAgents || isLoadingSettings;
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
await refreshFilesystemAgents();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleSource = (source: 'user' | 'project') => {
|
||||||
|
if (sources.includes(source)) {
|
||||||
|
updateSources(sources.filter((s: 'user' | 'project') => s !== source));
|
||||||
|
} else {
|
||||||
|
updateSources([...sources, source]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-2xl overflow-hidden',
|
||||||
|
'border border-border/50',
|
||||||
|
'bg-linear-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||||
|
'shadow-sm shadow-black/5'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-border/30">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-violet-500/20 flex items-center justify-center">
|
||||||
|
<Bot className="w-5 h-5 text-violet-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-base flex items-center gap-2">
|
||||||
|
Custom Subagents
|
||||||
|
{enabled && subagentsWithScope.length > 0 && (
|
||||||
|
<span className="text-xs font-normal px-2 py-0.5 rounded-full bg-violet-500/20 text-violet-500">
|
||||||
|
{subagentsWithScope.length} agent{subagentsWithScope.length !== 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mt-0.5">
|
||||||
|
Specialized agents Claude delegates to automatically
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="enable-subagents"
|
||||||
|
checked={enabled}
|
||||||
|
onCheckedChange={updateEnabled}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
{/* Sources Selection */}
|
||||||
|
{enabled && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
Load Subagents from
|
||||||
|
</Label>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
{/* User Subagents Option */}
|
||||||
|
<label
|
||||||
|
htmlFor="subagent-source-user"
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-3 p-3 rounded-xl border cursor-pointer transition-all duration-200',
|
||||||
|
sources.includes('user')
|
||||||
|
? 'border-violet-500/50 bg-violet-500/10'
|
||||||
|
: 'border-border/50 bg-accent/20 hover:bg-accent/30'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id="subagent-source-user"
|
||||||
|
checked={sources.includes('user')}
|
||||||
|
onCheckedChange={() => toggleSource('user')}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="data-[state=checked]:bg-violet-500 data-[state=checked]:border-violet-500"
|
||||||
|
/>
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-muted/50 flex items-center justify-center shrink-0">
|
||||||
|
<Globe className="w-4 h-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<span className="text-sm font-medium">User Subagents</span>
|
||||||
|
<span className="block text-xs text-muted-foreground mt-0.5 truncate">
|
||||||
|
~/.claude/agents/ — Available across all projects
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Project Subagents Option */}
|
||||||
|
<label
|
||||||
|
htmlFor="subagent-source-project"
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-3 p-3 rounded-xl border cursor-pointer transition-all duration-200',
|
||||||
|
sources.includes('project')
|
||||||
|
? 'border-violet-500/50 bg-violet-500/10'
|
||||||
|
: 'border-border/50 bg-accent/20 hover:bg-accent/30'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id="subagent-source-project"
|
||||||
|
checked={sources.includes('project')}
|
||||||
|
onCheckedChange={() => toggleSource('project')}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="data-[state=checked]:bg-violet-500 data-[state=checked]:border-violet-500"
|
||||||
|
/>
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-muted/50 flex items-center justify-center shrink-0">
|
||||||
|
<FolderOpen className="w-4 h-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<span className="text-sm font-medium">Project Subagents</span>
|
||||||
|
<span className="block text-xs text-muted-foreground mt-0.5 truncate">
|
||||||
|
.claude/agents/ — Version-controlled with project
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Agents List */}
|
||||||
|
{enabled && (
|
||||||
|
<>
|
||||||
|
{/* Refresh Button */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs uppercase tracking-wide text-muted-foreground">
|
||||||
|
Discovered Agents
|
||||||
|
</Label>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={isLoading}
|
||||||
|
title="Refresh agents from disk"
|
||||||
|
className="gap-1.5 h-7 px-2 text-xs"
|
||||||
|
>
|
||||||
|
{isLoadingAgents ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{subagentsWithScope.length === 0 ? (
|
||||||
|
<div className="text-center py-6 text-muted-foreground border border-dashed border-border/50 rounded-xl">
|
||||||
|
<Users className="w-10 h-10 mx-auto mb-2 opacity-30" />
|
||||||
|
<p className="text-sm font-medium">No agents found</p>
|
||||||
|
<p className="text-xs mt-1 max-w-sm mx-auto">
|
||||||
|
Create <code className="text-xs bg-muted px-1 rounded">.md</code> files in{' '}
|
||||||
|
{sources.includes('user') && (
|
||||||
|
<code className="text-xs bg-muted px-1 rounded">~/.claude/agents/</code>
|
||||||
|
)}
|
||||||
|
{sources.includes('user') && sources.includes('project') && ' or '}
|
||||||
|
{sources.includes('project') && (
|
||||||
|
<code className="text-xs bg-muted px-1 rounded">.claude/agents/</code>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{subagentsWithScope.map((agent) => (
|
||||||
|
<SubagentCard
|
||||||
|
key={`${agent.type}-${agent.source || agent.scope}-${agent.name}`}
|
||||||
|
agent={agent}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Help Text */}
|
||||||
|
{enabled && (
|
||||||
|
<div className="rounded-xl border border-border/30 bg-muted/30 p-4 space-y-3">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="w-6 h-6 rounded-md bg-brand-500/20 flex items-center justify-center shrink-0 mt-0.5">
|
||||||
|
<Sparkles className="w-3.5 h-3.5 text-brand-500" />
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground space-y-1">
|
||||||
|
<p className="font-medium text-foreground/80">Auto-Discovery</p>
|
||||||
|
<p>
|
||||||
|
Subagents are automatically discovered when agents start. Define agents as{' '}
|
||||||
|
<code className="text-xs bg-muted px-1 rounded">AGENT.md</code> files or{' '}
|
||||||
|
<code className="text-xs bg-muted px-1 rounded">agent-name.md</code> files.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="https://code.claude.com/docs/en/agents"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 text-xs text-brand-500 hover:text-brand-400 transition-colors"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
|
View Agents documentation
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Disabled State Empty Message */}
|
||||||
|
{!enabled && (
|
||||||
|
<div className="text-center py-6 text-muted-foreground">
|
||||||
|
<Bot className="w-10 h-10 mx-auto mb-3 opacity-30" />
|
||||||
|
<p className="text-sm">Subagents are disabled</p>
|
||||||
|
<p className="text-xs mt-1">Enable to load custom agent definitions</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -725,6 +725,83 @@ export interface ElectronAPI {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
ideation?: IdeationAPI;
|
ideation?: IdeationAPI;
|
||||||
|
settings?: {
|
||||||
|
getStatus: () => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
hasGlobalSettings: boolean;
|
||||||
|
hasCredentials: boolean;
|
||||||
|
dataDir: string;
|
||||||
|
needsMigration: boolean;
|
||||||
|
}>;
|
||||||
|
getGlobal: () => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
settings?: Record<string, unknown>;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
updateGlobal: (updates: Record<string, unknown>) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
settings?: Record<string, unknown>;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
getCredentials: () => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
credentials?: {
|
||||||
|
anthropic: { configured: boolean; masked: string };
|
||||||
|
google: { configured: boolean; masked: string };
|
||||||
|
openai: { configured: boolean; masked: string };
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
updateCredentials: (updates: {
|
||||||
|
apiKeys?: { anthropic?: string; google?: string; openai?: string };
|
||||||
|
}) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
credentials?: {
|
||||||
|
anthropic: { configured: boolean; masked: string };
|
||||||
|
google: { configured: boolean; masked: string };
|
||||||
|
openai: { configured: boolean; masked: string };
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
getProject: (projectPath: string) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
settings?: Record<string, unknown>;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
updateProject: (
|
||||||
|
projectPath: string,
|
||||||
|
updates: Record<string, unknown>
|
||||||
|
) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
settings?: Record<string, unknown>;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
migrate: (data: Record<string, string>) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
migratedGlobalSettings: boolean;
|
||||||
|
migratedCredentials: boolean;
|
||||||
|
migratedProjectCount: number;
|
||||||
|
errors: string[];
|
||||||
|
}>;
|
||||||
|
discoverAgents: (
|
||||||
|
projectPath?: string,
|
||||||
|
sources?: Array<'user' | 'project'>
|
||||||
|
) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
agents?: Array<{
|
||||||
|
name: string;
|
||||||
|
definition: {
|
||||||
|
description: string;
|
||||||
|
prompt: string;
|
||||||
|
tools?: string[];
|
||||||
|
model?: 'sonnet' | 'opus' | 'haiku' | 'inherit';
|
||||||
|
};
|
||||||
|
source: 'user' | 'project';
|
||||||
|
filePath: string;
|
||||||
|
}>;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Window interface is declared in @/types/electron.d.ts
|
// Note: Window interface is declared in @/types/electron.d.ts
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ import type {
|
|||||||
SpecRegenerationEvent,
|
SpecRegenerationEvent,
|
||||||
SuggestionType,
|
SuggestionType,
|
||||||
GitHubAPI,
|
GitHubAPI,
|
||||||
GitHubIssue,
|
|
||||||
GitHubPR,
|
|
||||||
IssueValidationInput,
|
IssueValidationInput,
|
||||||
IssueValidationEvent,
|
IssueValidationEvent,
|
||||||
IdeationAPI,
|
IdeationAPI,
|
||||||
@@ -374,7 +372,13 @@ export const verifySession = async (): Promise<boolean> => {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add session token header if available
|
// Electron mode: use API key header
|
||||||
|
const apiKey = getApiKey();
|
||||||
|
if (apiKey) {
|
||||||
|
headers['X-API-Key'] = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add session token header if available (web mode)
|
||||||
const sessionToken = getSessionToken();
|
const sessionToken = getSessionToken();
|
||||||
if (sessionToken) {
|
if (sessionToken) {
|
||||||
headers['X-Session-Token'] = sessionToken;
|
headers['X-Session-Token'] = sessionToken;
|
||||||
@@ -1877,6 +1881,26 @@ export class HttpApiClient implements ElectronAPI {
|
|||||||
migratedProjectCount: number;
|
migratedProjectCount: number;
|
||||||
errors: string[];
|
errors: string[];
|
||||||
}> => this.post('/api/settings/migrate', { data }),
|
}> => this.post('/api/settings/migrate', { data }),
|
||||||
|
|
||||||
|
// Filesystem agents discovery (read-only)
|
||||||
|
discoverAgents: (
|
||||||
|
projectPath?: string,
|
||||||
|
sources?: Array<'user' | 'project'>
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
agents?: Array<{
|
||||||
|
name: string;
|
||||||
|
definition: {
|
||||||
|
description: string;
|
||||||
|
prompt: string;
|
||||||
|
tools?: string[];
|
||||||
|
model?: 'sonnet' | 'opus' | 'haiku' | 'inherit';
|
||||||
|
};
|
||||||
|
source: 'user' | 'project';
|
||||||
|
filePath: string;
|
||||||
|
}>;
|
||||||
|
error?: string;
|
||||||
|
}> => this.post('/api/settings/agents/discover', { projectPath, sources }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sessions API
|
// Sessions API
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
import { createLogger } from '@automaker/utils/logger';
|
import { createLogger } from '@automaker/utils/logger';
|
||||||
import { getHttpApiClient } from './http-api-client';
|
import { getHttpApiClient } from './http-api-client';
|
||||||
import { getElectronAPI } from './electron';
|
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
|
||||||
const logger = createLogger('WorkspaceConfig');
|
const logger = createLogger('WorkspaceConfig');
|
||||||
@@ -33,9 +32,17 @@ function joinPath(...parts: string[]): string {
|
|||||||
*/
|
*/
|
||||||
async function getDefaultDocumentsPath(): Promise<string | null> {
|
async function getDefaultDocumentsPath(): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
// In Electron mode, use the native getPath API directly from the preload script
|
||||||
const documentsPath = await api.getPath('documents');
|
// This returns the actual system Documents folder (e.g., C:\Users\<user>\Documents on Windows)
|
||||||
return joinPath(documentsPath, 'Automaker');
|
// Note: The HTTP client's getPath returns incorrect Unix-style paths for 'documents'
|
||||||
|
if (typeof window !== 'undefined' && (window as any).electronAPI?.getPath) {
|
||||||
|
const documentsPath = await (window as any).electronAPI.getPath('documents');
|
||||||
|
return joinPath(documentsPath, 'Automaker');
|
||||||
|
}
|
||||||
|
|
||||||
|
// In web mode (no Electron), we can't access the user's Documents folder
|
||||||
|
// Return null to let the caller use other fallback mechanisms (like server's DATA_DIR)
|
||||||
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to get documents path:', error);
|
logger.error('Failed to get documents path:', error);
|
||||||
return null;
|
return null;
|
||||||
@@ -76,6 +83,7 @@ export async function getDefaultWorkspaceDirectory(): Promise<string | null> {
|
|||||||
|
|
||||||
// Try to get Documents/Automaker
|
// Try to get Documents/Automaker
|
||||||
const documentsPath = await getDefaultDocumentsPath();
|
const documentsPath = await getDefaultDocumentsPath();
|
||||||
|
logger.info('Default documentsPath resolved to:', documentsPath);
|
||||||
if (documentsPath) {
|
if (documentsPath) {
|
||||||
return documentsPath;
|
return documentsPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -585,6 +585,14 @@ export interface AppState {
|
|||||||
// MCP Servers
|
// MCP Servers
|
||||||
mcpServers: MCPServerConfig[]; // List of configured MCP servers for agent use
|
mcpServers: MCPServerConfig[]; // List of configured MCP servers for agent use
|
||||||
|
|
||||||
|
// Skills Configuration
|
||||||
|
enableSkills: boolean; // Enable Skills functionality (loads from .claude/skills/ directories)
|
||||||
|
skillsSources: Array<'user' | 'project'>; // Which directories to load Skills from
|
||||||
|
|
||||||
|
// Subagents Configuration
|
||||||
|
enableSubagents: boolean; // Enable Custom Subagents functionality (loads from .claude/agents/ directories)
|
||||||
|
subagentsSources: Array<'user' | 'project'>; // Which directories to load Subagents from
|
||||||
|
|
||||||
// Prompt Customization
|
// Prompt Customization
|
||||||
promptCustomization: PromptCustomization; // Custom prompts for Auto Mode, Agent, Backlog Plan, Enhancement
|
promptCustomization: PromptCustomization; // Custom prompts for Auto Mode, Agent, Backlog Plan, Enhancement
|
||||||
|
|
||||||
@@ -1188,6 +1196,10 @@ const initialState: AppState = {
|
|||||||
autoLoadClaudeMd: false, // Default to disabled (user must opt-in)
|
autoLoadClaudeMd: false, // Default to disabled (user must opt-in)
|
||||||
skipSandboxWarning: false, // Default to disabled (show sandbox warning dialog)
|
skipSandboxWarning: false, // Default to disabled (show sandbox warning dialog)
|
||||||
mcpServers: [], // No MCP servers configured by default
|
mcpServers: [], // No MCP servers configured by default
|
||||||
|
enableSkills: true, // Skills enabled by default
|
||||||
|
skillsSources: ['user', 'project'] as Array<'user' | 'project'>, // Load from both sources by default
|
||||||
|
enableSubagents: true, // Subagents enabled by default
|
||||||
|
subagentsSources: ['user', 'project'] as Array<'user' | 'project'>, // Load from both sources by default
|
||||||
promptCustomization: {}, // Empty by default - all prompts use built-in defaults
|
promptCustomization: {}, // Empty by default - all prompts use built-in defaults
|
||||||
aiProfiles: DEFAULT_AI_PROFILES,
|
aiProfiles: DEFAULT_AI_PROFILES,
|
||||||
projectAnalysis: null,
|
projectAnalysis: null,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export type {
|
|||||||
McpStdioServerConfig,
|
McpStdioServerConfig,
|
||||||
McpSSEServerConfig,
|
McpSSEServerConfig,
|
||||||
McpHttpServerConfig,
|
McpHttpServerConfig,
|
||||||
|
AgentDefinition,
|
||||||
ReasoningEffort,
|
ReasoningEffort,
|
||||||
} from './provider.js';
|
} from './provider.js';
|
||||||
|
|
||||||
@@ -189,6 +190,7 @@ export {
|
|||||||
addProviderPrefix,
|
addProviderPrefix,
|
||||||
getBareModelId,
|
getBareModelId,
|
||||||
normalizeModelString,
|
normalizeModelString,
|
||||||
|
validateBareModelId,
|
||||||
} from './provider-utils.js';
|
} from './provider-utils.js';
|
||||||
|
|
||||||
// Pipeline types
|
// Pipeline types
|
||||||
|
|||||||
@@ -240,3 +240,37 @@ export function normalizeModelString(model: string | undefined | null): string {
|
|||||||
|
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that a model ID does not contain a provider prefix
|
||||||
|
*
|
||||||
|
* Providers should receive bare model IDs (e.g., "gpt-5.1-codex-max", "composer-1")
|
||||||
|
* without provider prefixes (e.g., NOT "codex-gpt-5.1-codex-max", NOT "cursor-composer-1").
|
||||||
|
*
|
||||||
|
* This validation ensures the ProviderFactory properly stripped prefixes before
|
||||||
|
* passing models to providers.
|
||||||
|
*
|
||||||
|
* @param model - Model ID to validate
|
||||||
|
* @param providerName - Name of the provider for error messages
|
||||||
|
* @throws Error if model contains a provider prefix
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* validateBareModelId("gpt-5.1-codex-max", "CodexProvider"); // ✅ OK
|
||||||
|
* validateBareModelId("codex-gpt-5.1-codex-max", "CodexProvider"); // ❌ Throws error
|
||||||
|
*/
|
||||||
|
export function validateBareModelId(model: string, providerName: string): void {
|
||||||
|
if (!model || typeof model !== 'string') {
|
||||||
|
throw new Error(`[${providerName}] Invalid model ID: expected string, got ${typeof model}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [provider, prefix] of Object.entries(PROVIDER_PREFIXES)) {
|
||||||
|
if (model.startsWith(prefix)) {
|
||||||
|
throw new Error(
|
||||||
|
`[${providerName}] Model ID should not contain provider prefix '${prefix}'. ` +
|
||||||
|
`Got: '${model}'. ` +
|
||||||
|
`This is likely a bug in ProviderFactory - it should strip the '${provider}' prefix ` +
|
||||||
|
`before passing the model to the provider.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,12 +76,29 @@ export interface McpHttpServerConfig {
|
|||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subagent definition for specialized task delegation
|
||||||
|
*/
|
||||||
|
export interface AgentDefinition {
|
||||||
|
/** Natural language description of when to use this agent */
|
||||||
|
description: string;
|
||||||
|
/** System prompt defining the agent's role and behavior */
|
||||||
|
prompt: string;
|
||||||
|
/** Restricted tool list (if omitted, inherits all tools) */
|
||||||
|
tools?: string[];
|
||||||
|
/** Model override for this agent */
|
||||||
|
model?: 'sonnet' | 'opus' | 'haiku' | 'inherit';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for executing a query via a provider
|
* Options for executing a query via a provider
|
||||||
*/
|
*/
|
||||||
export interface ExecuteOptions {
|
export interface ExecuteOptions {
|
||||||
prompt: string | Array<{ type: string; text?: string; source?: object }>;
|
prompt: string | Array<{ type: string; text?: string; source?: object }>;
|
||||||
|
/** Bare model ID without provider prefix (e.g., "gpt-5.1-codex-max", "composer-1") */
|
||||||
model: string;
|
model: string;
|
||||||
|
/** Original model ID with provider prefix for logging (e.g., "codex-gpt-5.1-codex-max") */
|
||||||
|
originalModel?: string;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
systemPrompt?: string | SystemPromptPreset;
|
systemPrompt?: string | SystemPromptPreset;
|
||||||
maxTurns?: number;
|
maxTurns?: number;
|
||||||
@@ -107,6 +124,11 @@ export interface ExecuteOptions {
|
|||||||
* Only applies to Claude models; Cursor models handle thinking internally.
|
* Only applies to Claude models; Cursor models handle thinking internally.
|
||||||
*/
|
*/
|
||||||
thinkingLevel?: ThinkingLevel;
|
thinkingLevel?: ThinkingLevel;
|
||||||
|
/**
|
||||||
|
* Custom subagents for specialized task delegation
|
||||||
|
* Key: agent name, Value: agent definition
|
||||||
|
*/
|
||||||
|
agents?: Record<string, AgentDefinition>;
|
||||||
/**
|
/**
|
||||||
* Reasoning effort for Codex/OpenAI models with reasoning capabilities.
|
* Reasoning effort for Codex/OpenAI models with reasoning capabilities.
|
||||||
* Controls how many reasoning tokens the model generates before responding.
|
* Controls how many reasoning tokens the model generates before responding.
|
||||||
|
|||||||
@@ -564,6 +564,43 @@ export interface GlobalSettings {
|
|||||||
// Prompt Customization
|
// Prompt Customization
|
||||||
/** Custom prompts for Auto Mode, Agent Runner, Backlog Planning, and Enhancements */
|
/** Custom prompts for Auto Mode, Agent Runner, Backlog Planning, and Enhancements */
|
||||||
promptCustomization?: PromptCustomization;
|
promptCustomization?: PromptCustomization;
|
||||||
|
|
||||||
|
// Skills Configuration
|
||||||
|
/**
|
||||||
|
* Enable Skills functionality (loads from .claude/skills/ directories)
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
enableSkills?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which directories to load Skills from
|
||||||
|
* - 'user': ~/.claude/skills/ (personal skills)
|
||||||
|
* - 'project': .claude/skills/ (project-specific skills)
|
||||||
|
* @default ['user', 'project']
|
||||||
|
*/
|
||||||
|
skillsSources?: Array<'user' | 'project'>;
|
||||||
|
|
||||||
|
// Subagents Configuration
|
||||||
|
/**
|
||||||
|
* Enable Custom Subagents functionality (loads from .claude/agents/ directories)
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
enableSubagents?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which directories to load Subagents from
|
||||||
|
* - 'user': ~/.claude/agents/ (personal agents)
|
||||||
|
* - 'project': .claude/agents/ (project-specific agents)
|
||||||
|
* @default ['user', 'project']
|
||||||
|
*/
|
||||||
|
subagentsSources?: Array<'user' | 'project'>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom subagent definitions for specialized task delegation (programmatic)
|
||||||
|
* Key: agent name (e.g., 'code-reviewer', 'test-runner')
|
||||||
|
* Value: agent configuration
|
||||||
|
*/
|
||||||
|
customSubagents?: Record<string, import('./provider.js').AgentDefinition>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -663,6 +700,15 @@ export interface ProjectSettings {
|
|||||||
// Claude Agent SDK Settings
|
// Claude Agent SDK Settings
|
||||||
/** Auto-load CLAUDE.md files using SDK's settingSources option (project override) */
|
/** Auto-load CLAUDE.md files using SDK's settingSources option (project override) */
|
||||||
autoLoadClaudeMd?: boolean;
|
autoLoadClaudeMd?: boolean;
|
||||||
|
|
||||||
|
// Subagents Configuration
|
||||||
|
/**
|
||||||
|
* Project-specific custom subagent definitions for specialized task delegation
|
||||||
|
* Merged with global customSubagents, project-level takes precedence
|
||||||
|
* Key: agent name (e.g., 'code-reviewer', 'test-runner')
|
||||||
|
* Value: agent configuration
|
||||||
|
*/
|
||||||
|
customSubagents?: Record<string, import('./provider.js').AgentDefinition>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -766,6 +812,10 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
|
|||||||
codexAdditionalDirs: DEFAULT_CODEX_ADDITIONAL_DIRS,
|
codexAdditionalDirs: DEFAULT_CODEX_ADDITIONAL_DIRS,
|
||||||
codexThreadId: undefined,
|
codexThreadId: undefined,
|
||||||
mcpServers: [],
|
mcpServers: [],
|
||||||
|
enableSkills: true,
|
||||||
|
skillsSources: ['user', 'project'],
|
||||||
|
enableSubagents: true,
|
||||||
|
subagentsSources: ['user', 'project'],
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Default credentials (empty strings - user must provide API keys) */
|
/** Default credentials (empty strings - user must provide API keys) */
|
||||||
|
|||||||
Reference in New Issue
Block a user