-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat: Memory system improvements + Custom Agents integration #1920
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
vitorafgomes
wants to merge
49
commits into
AndyMik90:develop
Choose a base branch
from
vitorafgomes:feature/consolidated-memory-and-agents
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 35 commits
Commits
Show all changes
49 commits
Select commit
Hold shift + click to select a range
3f1c769
chore: bump version to 2.7.7
vitorfgomes edda06f
feat: Add Customer flow - Phase 1
vitorfgomes f540922
chore: bump version to 2.7.8
vitorfgomes 0e5c7e6
fix: customer init skips git check, creates .auto-claude directly
vitorfgomes 308eb74
fix: pass updated project with autoBuildPath to GitHubSetupModal
vitorfgomes 1ec12ad
feat: add initializeCustomerProject IPC for git-free .auto-claude setup
vitorfgomes 98727e2
feat: multi-repo GitHub Issues for Customer projects
vitorfgomes 2648cb8
chore: bump version to 2.7.9
vitorfgomes dda7566
fix: show GitHub nav for Customer projects and their child repos
vitorfgomes 4e9e570
chore: bump version to 2.7.10
vitorfgomes cc064df
fix: keep GitHub nav visible when child repo selected via Customer dr…
vitorfgomes 06bb177
fix: detect child repo GitHub config from git remote origin
vitorfgomes bce3a24
feat: multi-repo GitHub PRs for Customer projects
vitorfgomes cd7747b
chore: bump version to 2.7.11
vitorfgomes 6f713f1
feat: display Claude Code global MCPs in MCP Server Overview
vitorfgomes 33f60bf
chore: bump version to 2.7.12
vitorfgomes b136a99
feat: read global MCPs from ~/.claude.json and display custom agents
vitorfgomes 1994439
chore: bump version to 2.7.13
vitorfgomes db3b5fc
feat: enable investigation, auto-fix, and PR review for customer mult…
vitorfgomes b629cba
chore: bump version to 2.7.14
vitorfgomes f2b5acf
feat: fix project indexing for customer projects + add .NET, docs & m…
vitorfgomes 69184e1
Merge pull request #1 from vitorafgomes/feat/customer-project-indexing
vitorafgomes 3a37fcf
fix: remove unused import and handle XML namespaces in .csproj parser
vitorfgomes 95ffde7
fix: address PR review feedback — DRY, specific exceptions, ES imports
vitorfgomes 6057cba
fix: address CodeRabbit PR review findings — security, cross-platform…
vitorfgomes 20d982d
fix: ruff format + fix test assertions for addProject type parameter
vitorfgomes a22302b
fix: resolve remaining PR review findings — MCP validation, i18n, asy…
vitorfgomes ab5714e
fix: resolve 50+ PR review items — multi-repo identity, i18n, path co…
vitorfgomes 51abab3
fix: revert @shared aliases in main/preload — not supported by electr…
vitorfgomes bfe5756
fix: resolve remaining backend review items — .NET solution, routes, …
vitorfgomes cac0374
fix: resolve remaining PR #1908 review items — security, i18n, access…
vitorfgomes 16fcc91
feat(memory): improve Graphiti memory system with filtering, TTL, sco…
vitorfgomes 65a862f
feat: integrate custom agents from ~/.claude/agents/ into build pipeline
vitorfgomes 4bec08b
feat(memory): add frontend embedding dimension helpers and IPC improv…
vitorfgomes d154ff9
refactor: make all custom agents automatically available instead of m…
vitorfgomes aa0c15a
feat: add health status and pipeline phase assignment for Global MCPs
vitorfgomes bcaba0a
fix: use dedicated health check for global MCPs without command allow…
vitorfgomes 145fba7
fix: resolve PR review comments + load global MCPs from ~/.claude.json
vitorfgomes 6e67376
fix: resolve remaining PR review comments (round 2)
vitorfgomes d2a8a8a
fix: remaining frontend review comments (AgentTools, settings)
vitorfgomes d8d7b71
chore: bump version to 2.7.15
vitorfgomes bcd091b
fix: final frontend review comments (i18n, path aliases, cleanup)
vitorfgomes 3e9ab01
fix: pass env vars to MCP servers and filter disabled MCPs
vitorfgomes 8104c46
fix: resolve PR #1920 review comments (critical + actionable items)
vitorfgomes 8469f45
fix: isolate CLAUDE_CONFIG_DIR in auth/client tests
vitorfgomes ef2c4ac
fix: eliminate TOCTOU race in MCP plugin cache reader
vitorfgomes 43a1a3d
Merge branch 'develop' into feature/consolidated-memory-and-agents
vitorafgomes 9fe6eeb
fix: respect CLAUDE_CONFIG_DIR in fast_mode and export getUserConfigDir
vitorfgomes 8eaa165
chore: bump version to 2.7.16
vitorfgomes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,270 @@ | ||
| """ | ||
| Custom Agents Integration | ||
| ========================= | ||
|
|
||
| Parses custom agent .md files from ~/.claude/agents/ and provides | ||
| their configuration (system prompt, optional tool/MCP overrides) | ||
| for use in the Auto-Claude build pipeline. | ||
|
|
||
| Custom agent files are markdown files that may include YAML frontmatter | ||
| for tool and MCP server configuration: | ||
|
|
||
| --- | ||
| tools: [Read, Write, Edit, Bash, Glob, Grep] | ||
| mcp_servers: [context7, graphiti] | ||
| thinking: high | ||
| --- | ||
| You are a frontend specialist... | ||
|
|
||
| If no frontmatter is provided, only the system prompt (markdown body) | ||
| is used, and tool/MCP configuration comes from the base AGENT_CONFIGS. | ||
| """ | ||
|
|
||
| import logging | ||
| import os | ||
| import re | ||
| from dataclasses import dataclass, field | ||
| from pathlib import Path | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| @dataclass | ||
| class CustomAgentConfig: | ||
| """Parsed configuration from a custom agent .md file.""" | ||
|
|
||
| agent_id: str | ||
| system_prompt: str | ||
| tools: list[str] | None = None # Override base tools (None = use defaults) | ||
| mcp_servers: list[str] | None = None # Override MCP servers (None = use defaults) | ||
| thinking: str | None = None # Override thinking level (None = use default) | ||
| raw_frontmatter: dict = field(default_factory=dict) | ||
|
|
||
|
|
||
| def get_agents_dir() -> Path: | ||
| """Get the custom agents directory (~/.claude/agents/).""" | ||
| config_dir = os.environ.get("CLAUDE_CONFIG_DIR") or os.path.join( | ||
| os.path.expanduser("~"), ".claude" | ||
| ) | ||
| return Path(config_dir) / "agents" | ||
|
|
||
|
|
||
| def parse_agent_file(file_path: Path) -> CustomAgentConfig | None: | ||
| """ | ||
| Parse a custom agent .md file. | ||
|
|
||
| Extracts optional YAML frontmatter (between --- delimiters) and the | ||
| markdown body as the system prompt. | ||
|
|
||
| Args: | ||
| file_path: Path to the .md agent file | ||
|
|
||
| Returns: | ||
| CustomAgentConfig if file is valid, None if file doesn't exist or is invalid | ||
| """ | ||
| if not file_path.exists() or not file_path.is_file(): | ||
| logger.warning(f"Custom agent file not found: {file_path}") | ||
| return None | ||
|
|
||
| try: | ||
| content = file_path.read_text(encoding="utf-8") | ||
| except Exception as e: | ||
| logger.warning(f"Failed to read custom agent file {file_path}: {e}") | ||
| return None | ||
|
|
||
| agent_id = file_path.stem # filename without .md | ||
| frontmatter = {} | ||
| body = content | ||
|
|
||
| # Extract YAML frontmatter if present | ||
| fm_match = re.match(r"^---\s*\n(.*?)\n---\s*\n(.*)", content, re.DOTALL) | ||
| if fm_match: | ||
| fm_text = fm_match.group(1) | ||
| body = fm_match.group(2).strip() | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # Simple YAML-like parsing (avoid heavy yaml dependency) | ||
| frontmatter = _parse_simple_yaml(fm_text) | ||
|
|
||
| system_prompt = body.strip() | ||
| if not system_prompt: | ||
| logger.warning(f"Custom agent file has no content: {file_path}") | ||
| return None | ||
|
|
||
| # Extract optional overrides from frontmatter | ||
| tools = _parse_string_list(frontmatter.get("tools")) | ||
| mcp_servers = _parse_string_list(frontmatter.get("mcp_servers")) | ||
| thinking = frontmatter.get("thinking") | ||
|
|
||
| if thinking and thinking not in ("low", "medium", "high"): | ||
| logger.warning( | ||
| f"Invalid thinking level '{thinking}' in {file_path}, ignoring" | ||
| ) | ||
| thinking = None | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return CustomAgentConfig( | ||
| agent_id=agent_id, | ||
| system_prompt=system_prompt, | ||
| tools=tools, | ||
| mcp_servers=mcp_servers, | ||
| thinking=thinking, | ||
| raw_frontmatter=frontmatter, | ||
| ) | ||
|
|
||
|
|
||
| def load_custom_agent(agent_id: str) -> CustomAgentConfig | None: | ||
| """ | ||
| Load a custom agent by ID. | ||
|
|
||
| Searches through category directories in ~/.claude/agents/ for | ||
| a matching agent file. | ||
|
|
||
| Args: | ||
| agent_id: Agent ID (filename without .md extension) | ||
|
|
||
| Returns: | ||
| CustomAgentConfig if found, None otherwise | ||
| """ | ||
| agents_dir = get_agents_dir() | ||
| if not agents_dir.exists(): | ||
| return None | ||
|
|
||
| # Search in all category directories | ||
| for category_dir in sorted(agents_dir.iterdir()): | ||
| if not category_dir.is_dir(): | ||
| continue | ||
| agent_file = category_dir / f"{agent_id}.md" | ||
| if agent_file.exists(): | ||
| return parse_agent_file(agent_file) | ||
|
|
||
| # Also check root agents dir (no category) | ||
| root_file = agents_dir / f"{agent_id}.md" | ||
| if root_file.exists(): | ||
| return parse_agent_file(root_file) | ||
|
|
||
| logger.debug(f"Custom agent '{agent_id}' not found in {agents_dir}") | ||
| return None | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| def load_all_agents() -> list[CustomAgentConfig]: | ||
| """Load all custom agents from all categories in ~/.claude/agents/.""" | ||
| agents_dir = get_agents_dir() | ||
| if not agents_dir.exists(): | ||
| return [] | ||
|
|
||
| agents = [] | ||
| for category_dir in sorted(agents_dir.iterdir()): | ||
| if not category_dir.is_dir(): | ||
| continue | ||
| for agent_file in sorted(category_dir.glob("*.md")): | ||
| if agent_file.name == "README.md": | ||
| continue | ||
| agent = parse_agent_file(agent_file) | ||
| if agent: | ||
| agents.append(agent) | ||
| return agents | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| def build_agents_catalog_prompt() -> str | None: | ||
| """ | ||
| Build a concise catalog of all available custom agents for system prompt injection. | ||
|
|
||
| Returns a formatted string listing all agents by category with their descriptions, | ||
| or None if no agents are available. | ||
| """ | ||
| agents_dir = get_agents_dir() | ||
| if not agents_dir.exists(): | ||
| return None | ||
|
|
||
| categories: list[tuple[str, list[tuple[str, str]]]] = [] | ||
|
|
||
| for category_dir in sorted(agents_dir.iterdir()): | ||
| if not category_dir.is_dir(): | ||
| continue | ||
| category_name = category_dir.name.split("-", 1)[-1].replace("-", " ").title() if "-" in category_dir.name else category_dir.name | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| agent_entries = [] | ||
| for agent_file in sorted(category_dir.glob("*.md")): | ||
| if agent_file.name == "README.md": | ||
| continue | ||
| agent = parse_agent_file(agent_file) | ||
| if agent: | ||
| # Get description from frontmatter, or first line of prompt | ||
| description = agent.raw_frontmatter.get("description", "") | ||
| if not description: | ||
| # Use first sentence of system prompt as fallback | ||
| first_line = agent.system_prompt.split("\n")[0].strip() | ||
| description = first_line[:120] | ||
| elif len(description) > 150: | ||
| description = description[:147] + "..." | ||
| agent_entries.append((agent.agent_id, description)) | ||
|
|
||
| if agent_entries: | ||
| categories.append((category_name, agent_entries)) | ||
|
|
||
| if not categories: | ||
| return None | ||
|
|
||
| total = sum(len(entries) for _, entries in categories) | ||
| lines = [ | ||
| f"# Available Specialist Agents ({total} agents)", | ||
| "", | ||
| "You have access to the following specialist agents organized by category.", | ||
| "Use them when the task requires specialized expertise — spawn them as subagents", | ||
| "via the Agent tool with the appropriate subagent_type.", | ||
| "", | ||
| ] | ||
|
|
||
| for category_name, entries in categories: | ||
| lines.append(f"## {category_name}") | ||
| for agent_id, desc in entries: | ||
| lines.append(f"- **{agent_id}**: {desc}") | ||
| lines.append("") | ||
|
|
||
| return "\n".join(lines) | ||
|
|
||
|
|
||
| def _parse_simple_yaml(text: str) -> dict: | ||
| """ | ||
| Parse simple YAML-like frontmatter (key: value pairs). | ||
|
|
||
| Handles: | ||
| - key: value (strings) | ||
| - key: [item1, item2] (inline lists) | ||
| - key: (empty value) | ||
|
|
||
| Does NOT handle nested structures or multi-line values. | ||
| """ | ||
| result = {} | ||
| for line in text.split("\n"): | ||
| line = line.strip() | ||
| if not line or line.startswith("#"): | ||
| continue | ||
| if ":" not in line: | ||
| continue | ||
| key, _, value = line.partition(":") | ||
| key = key.strip() | ||
| value = value.strip() | ||
|
|
||
| # Parse inline list: [item1, item2] | ||
| if value.startswith("[") and value.endswith("]"): | ||
| items = value[1:-1].split(",") | ||
| result[key] = [item.strip().strip("\"'") for item in items if item.strip()] | ||
| elif value: | ||
| # Strip quotes | ||
| result[key] = value.strip("\"'") | ||
| else: | ||
| result[key] = "" | ||
|
|
||
| return result | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| def _parse_string_list(value) -> list[str] | None: | ||
vitorafgomes marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """Parse a value as a list of strings, or None if empty/invalid.""" | ||
| if value is None: | ||
| return None | ||
| if isinstance(value, list): | ||
| cleaned = [str(v).strip() for v in value if v] | ||
| return cleaned if cleaned else None | ||
| if isinstance(value, str) and value: | ||
| return [v.strip() for v in value.split(",") if v.strip()] | ||
| return None | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.