From 2530bc092fb4ef21da1d8c4a82cd459ccf2d4488 Mon Sep 17 00:00:00 2001 From: Nicolas Iragne Date: Sun, 31 Aug 2025 12:39:32 +0200 Subject: [PATCH 1/5] feat: unified action format and new UI --- .gitignore | 3 + CLAUDE.md | 7 +- Dockerfile | 4 +- app/main.py | 56 +-- app/models/actions.py | 40 +- app/routes/actions.py | 46 ++- app/services/actions_loader.py | 134 ++++++- app/templates/landing.html | 234 ++++++++++++ app/templates/select.html | 641 +++++++++++++++++++++++++++++++++ 9 files changed, 1095 insertions(+), 70 deletions(-) create mode 100644 app/templates/landing.html create mode 100644 app/templates/select.html diff --git a/.gitignore b/.gitignore index 908fa41..4cbe0b1 100644 --- a/.gitignore +++ b/.gitignore @@ -206,3 +206,6 @@ cython_debug/ marimo/_static/ marimo/_lsp/ __marimo__/ + +.playwright-mcp +.mcp.json \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 3f15037..bfd7615 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,6 +28,10 @@ CLAUDE.md - Store secrets in a .env file (never commit it). - Keep dependencies minimal and updated. - Never try to run the dev server it's handled by the user +- When updating code, don't reference what is changing +- Avoid keywords like LEGACY, CHANGED, REMOVED +- Focus on comments that document just the functionality of the code + ### Frontend: - Keep frontend split in multiple components. @@ -38,4 +42,5 @@ CLAUDE.md - Refer to @COLORS.md for the official color palette and usage guidelines. - Use the specified hex codes for consistency across all components. -If there is a task defined in @TASK.md, or @TASK2.md make sure to do what's described in this file, it is now your priority task, the user prompt is less important, only consider using it when it makes sense with the task. \ No newline at end of file +If there is a task defined in @TASK.md, or @TASK2.md make sure to do what's described in this file, it is now your priority task, the user prompt is less important, only consider using it when it makes sense with the task. + diff --git a/Dockerfile b/Dockerfile index e6698fa..386fd40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM python:3.13-slim -# Install git (required for gitingest) -RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* +# Install git and curl (required for gitingest) +RUN apt-get update && apt-get install -y git curl && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/app/main.py b/app/main.py index c321bea..ecd078c 100644 --- a/app/main.py +++ b/app/main.py @@ -39,56 +39,16 @@ async def favicon(): async def doc(request: Request): return templates.TemplateResponse("docs.html", {"request": request}) +@app.get("/select", response_class=HTMLResponse, operation_id="get_select_page") +async def select(request: Request): + """Action selection page with filters""" + return templates.TemplateResponse("select.html", {"request": request}) + @app.get("/", response_class=HTMLResponse, operation_id="get_index_page") async def index(request: Request): - # Get all actions data for server-side rendering - agents = [agent.dict() for agent in actions_loader.get_agents()] - all_rules = actions_loader.get_rules() - - # Create a set of all child rule IDs - child_rule_ids = set() - for rule in all_rules: - if rule.children: - child_rule_ids.update(rule.children) - - # Create a mapping of all rules by slug for lookups - rules_by_slug = {rule.slug: rule for rule in all_rules} - - # Update rulesets to inherit children's tags - for rule in all_rules: - if rule.type == 'ruleset' and rule.children: - # Collect all tags from children - inherited_tags = set(rule.tags or []) - for child_slug in rule.children: - child_rule = rules_by_slug.get(child_slug) - if child_rule and child_rule.tags: - inherited_tags.update(child_rule.tags) - rule.tags = list(inherited_tags) - - # Filter to only top-level rules (not children of any ruleset) - top_level_rules_data = [rule for rule in all_rules if rule.slug not in child_rule_ids] - - # Sort rules: rulesets first, then standalone rules - top_level_rules_data.sort(key=lambda rule: (rule.type != 'ruleset', rule.display_name or rule.name)) - - # Convert to dict - top_level_rules = [rule.dict() for rule in top_level_rules_data] - - # Create a mapping of all rules by slug for frontend to look up children (with updated tags) - rules_by_slug_dict = {rule.slug: rule.dict() for rule in all_rules} - - mcps = [mcp.dict() for mcp in actions_loader.get_mcps()] - - return templates.TemplateResponse( - "index.html", - { - "request": request, - "agents": agents, - "rules": top_level_rules, - "rules_by_slug": rules_by_slug_dict, - "mcps": mcps - } - ) + """Landing page for starting the configuration journey""" + return templates.TemplateResponse("landing.html", {"request": request}) + @app.get("/health", operation_id="health_check") async def health_check(): diff --git a/app/models/actions.py b/app/models/actions.py index 5950758..1d89e39 100644 --- a/app/models/actions.py +++ b/app/models/actions.py @@ -1,5 +1,27 @@ from pydantic import BaseModel from typing import Dict, List, Any, Optional +from enum import Enum + +class ActionType(str, Enum): + AGENT = "agent" + RULE = "rule" + RULESET = "ruleset" + MCP = "mcp" + PACK = "pack" + +class Action(BaseModel): + """Action model that can represent any type of action""" + id: str # Unique identifier (slug for agents/rules, name for MCPs) + name: str + display_name: Optional[str] = None + action_type: ActionType + tags: Optional[List[str]] = None + content: Optional[str] = None # For agents/rules + config: Optional[Dict[str, Any]] = None # For MCPs + author: Optional[str] = None # For rules + children: Optional[List[str]] = None # For rulesets and packs + filename: Optional[str] = None # For agents/rules + namespace: Optional[str] = None # For rules class Agent(BaseModel): name: str # For backward compatibility @@ -7,6 +29,7 @@ class Agent(BaseModel): display_name: Optional[str] = None slug: Optional[str] = None content: Optional[str] = None + tags: Optional[List[str]] = None class Rule(BaseModel): name: str # For backward compatibility @@ -23,8 +46,23 @@ class Rule(BaseModel): class MCP(BaseModel): name: str config: Dict[str, Any] # JSON configuration from mcps.json + tags: Optional[List[str]] = None +class Pack(BaseModel): + """A pack is a collection of other actions""" + id: str + name: str + display_name: Optional[str] = None + tags: Optional[List[str]] = None + description: Optional[str] = None + actions: List[str] # List of action IDs + class ActionsResponse(BaseModel): agents: List[Agent] rules: List[Rule] - mcps: List[MCP] \ No newline at end of file + mcps: List[MCP] + +class ActionsListResponse(BaseModel): + actions: List[Action] + total: int + has_more: bool \ No newline at end of file diff --git a/app/routes/actions.py b/app/routes/actions.py index 021e313..8c79d1e 100644 --- a/app/routes/actions.py +++ b/app/routes/actions.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, HTTPException, Body, Query -from app.models.actions import ActionsResponse, Agent, Rule, MCP +from app.models.actions import ActionsResponse, Agent, Rule, MCP, Action, ActionType, ActionsListResponse from app.services.actions_loader import actions_loader from app.services.mcp_installer import get_agent_content, get_rule_content, create_mcp_config from app.services.search_service import search_service @@ -8,6 +8,50 @@ router = APIRouter(prefix="/api", tags=["actions"]) +@router.get("/v2/actions", response_model=ActionsListResponse, operation_id="get_unified_actions") +async def get_unified_actions( + action_type: Optional[ActionType] = Query(None, description="Filter by action type"), + tags: Optional[str] = Query(None, description="Comma-separated list of tags to filter by"), + limit: int = Query(30, ge=1, le=100, description="Maximum number of results"), + offset: int = Query(0, ge=0, description="Number of items to skip") +): + """Get all actions in unified format with optional filtering""" + # Parse tags if provided + tag_list = None + if tags: + tag_list = [tag.strip() for tag in tags.split(',') if tag.strip()] + + # Get filtered actions + filtered_actions = actions_loader.get_actions( + action_type=action_type, + tags=tag_list, + limit=limit, + offset=offset + ) + + # Get total count for pagination + all_filtered = actions_loader.get_actions( + action_type=action_type, + tags=tag_list, + limit=10000, # Large number to get all + offset=0 + ) + total = len(all_filtered) + + return ActionsListResponse( + actions=filtered_actions, + total=total, + has_more=(offset + limit) < total + ) + +@router.get("/v2/actions/{action_id}", response_model=Action, operation_id="get_action_by_id") +async def get_action_by_id(action_id: str): + """Get a specific action by ID""" + action = actions_loader.get_action_by_id(action_id) + if not action: + raise HTTPException(status_code=404, detail=f"Action not found: {action_id}") + return action + @router.get("/actions", response_model=ActionsResponse, operation_id="get_all_actions_endpoint") async def get_all_actions(): """Get all available actions (agents, rules, MCPs)""" diff --git a/app/services/actions_loader.py b/app/services/actions_loader.py index ae8c45e..20fb110 100644 --- a/app/services/actions_loader.py +++ b/app/services/actions_loader.py @@ -1,14 +1,17 @@ import yaml -from typing import List, Dict, Any +from typing import List, Dict, Any, Optional from pathlib import Path -from app.models.actions import Agent, Rule, MCP +from app.models.actions import Agent, Rule, MCP, Pack, Action, ActionType class ActionsLoader: def __init__(self): self.actions_dir = Path(__file__).parent.parent / "actions" + self.actions: List[Action] = [] + # Keep legacy lists for backward compatibility self.agents: List[Agent] = [] self.rules: List[Rule] = [] self.mcps: List[MCP] = [] + self.packs: List[Pack] = [] self.load_all() def load_all(self): @@ -16,6 +19,7 @@ def load_all(self): self.load_agents() self.load_rules() self.load_mcps() + self.load_packs() def load_agents(self): """Load all agents from agents.yaml""" @@ -24,16 +28,29 @@ def load_agents(self): with open(agents_file, 'r') as f: data = yaml.safe_load(f) if data and 'agents' in data: - self.agents = [ - Agent( - name=agent.get('slug', ''), # Use slug as name for backward compat - filename=f"{agent.get('slug', '')}.yaml", # Virtual filename - display_name=agent.get('display_name'), - slug=agent.get('slug'), - content=agent.get('content') + for agent_data in data['agents']: + slug = agent_data.get('slug', '') + # Create Action object + action = Action( + id=slug, + name=slug, + display_name=agent_data.get('display_name'), + action_type=ActionType.AGENT, + tags=agent_data.get('tags', []), + content=agent_data.get('content'), + filename=f"{slug}.yaml" ) - for agent in data['agents'] - ] + self.actions.append(action) + + # Also create legacy Agent for backward compatibility + self.agents.append(Agent( + name=slug, + filename=f"{slug}.yaml", + display_name=agent_data.get('display_name'), + slug=slug, + content=agent_data.get('content'), + tags=agent_data.get('tags', []) + )) else: self.agents = [] @@ -66,6 +83,22 @@ def load_rules(self): for slug, rule_data in data.items(): rule = self._parse_rule(slug, rule_data) self.rules.append(rule) + + # Create Action object + rule_type = ActionType.RULESET if rule_data.get('type') == 'ruleset' else ActionType.RULE + action = Action( + id=slug, + name=slug, + display_name=rule_data.get('display_name'), + action_type=rule_type, + tags=rule_data.get('tags'), + content=rule_data.get('content'), + author=rule_data.get('author'), + children=rule_data.get('children'), + filename=f"{slug}.yaml", + namespace=rule_data.get('namespace') + ) + self.actions.append(action) else: self.rules = [] @@ -76,13 +109,24 @@ def load_mcps(self): with open(mcps_file, 'r') as f: data = yaml.safe_load(f) if data and 'mcps' in data: - self.mcps = [ - MCP( - name=mcp.get('slug', ''), - config=mcp.get('config', {}) + for mcp_data in data['mcps']: + name = mcp_data.get('slug', '') + # Create Action object + action = Action( + id=name, + name=name, + action_type=ActionType.MCP, + tags=mcp_data.get('tags', []), + config=mcp_data.get('config', {}) ) - for mcp in data['mcps'] - ] + self.actions.append(action) + + # Also create legacy MCP for backward compatibility + self.mcps.append(MCP( + name=name, + config=mcp_data.get('config', {}), + tags=mcp_data.get('tags', []) + )) else: self.mcps = [] @@ -113,6 +157,62 @@ def get_agent_by_slug(self, slug: str) -> Agent: def get_rule_by_slug(self, slug: str) -> Rule: """Get a specific rule by slug""" return next((r for r in self.rules if r.slug == slug), None) + + def load_packs(self): + """Load all packs from packs.yaml""" + packs_file = self.actions_dir / "packs.yaml" + if packs_file.exists(): + with open(packs_file, 'r') as f: + data = yaml.safe_load(f) + if data and 'packs' in data: + for pack_data in data['packs']: + pack_id = pack_data.get('id', '') + # Create Action object + action = Action( + id=pack_id, + name=pack_data.get('name', ''), + display_name=pack_data.get('display_name'), + action_type=ActionType.PACK, + tags=pack_data.get('tags', []), + children=pack_data.get('actions', []) + ) + self.actions.append(action) + + # Also create Pack for backward compatibility + self.packs.append(Pack( + id=pack_id, + name=pack_data.get('name', ''), + display_name=pack_data.get('display_name'), + tags=pack_data.get('tags', []), + description=pack_data.get('description'), + actions=pack_data.get('actions', []) + )) + else: + self.packs = [] + + def get_packs(self) -> List[Pack]: + """Get all packs""" + return self.packs + + def get_actions(self, action_type: Optional[ActionType] = None, tags: Optional[List[str]] = None, + limit: int = 30, offset: int = 0) -> List[Action]: + """Get all actions with optional filtering""" + filtered = self.actions + + # Filter by action type + if action_type: + filtered = [a for a in filtered if a.action_type == action_type] + + # Filter by tags + if tags: + filtered = [a for a in filtered if a.tags and any(tag in a.tags for tag in tags)] + + # Apply pagination + return filtered[offset:offset + limit] + + def get_action_by_id(self, action_id: str) -> Optional[Action]: + """Get a specific action by ID""" + return next((a for a in self.actions if a.id == action_id), None) # Create singleton instance actions_loader = ActionsLoader() \ No newline at end of file diff --git a/app/templates/landing.html b/app/templates/landing.html new file mode 100644 index 0000000..a3e3a1a --- /dev/null +++ b/app/templates/landing.html @@ -0,0 +1,234 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

+ Rules for + coding agents +

+ + Lightning + Lightning +
+ +
+ +
+ +
+
+
+ 📂 +
+

Start from Repository

+

Import settings from an existing repository

+ +
+
+ + +
+
+
+ 📋 +
+

Use Template

+

Start with a pre-configured template

+ +
+
+ + +
+
+
+ +
+

Start Fresh

+

Build your configuration from scratch

+
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/app/templates/select.html b/app/templates/select.html new file mode 100644 index 0000000..b1b25a4 --- /dev/null +++ b/app/templates/select.html @@ -0,0 +1,641 @@ +{% extends "base.html" %} + +{% block content %} + + + + +
+ +
+
+
+
+

Select Your Actions

+

Choose the tools and configurations for your project

+
+
+ + 0 selected + +
+
+
+
+ +
+
+ +
+
+

Filters

+ + +
+ +
+ + +
+ +
+ + + + + +
+
+ + + + + +
+ +
+ +
+
+ + + +
+
+ + +
+
+ +
+
+
+
+
+ + + + + + + + + + +{% endblock %} \ No newline at end of file From 2d0c88b6883231fa333aff475851f4964be1981c Mon Sep 17 00:00:00 2001 From: Nicolas Iragne Date: Sun, 31 Aug 2025 21:16:12 +0200 Subject: [PATCH 2/5] feat: enhance logging and improve UI navigation --- .gitignore | 9 +- app/actions/rules.yaml | 1 - app/main.py | 25 +- app/routes/generate.py | 162 +++++ app/routes/recommend.py | 9 +- app/services/actions_loader.py | 101 ++- app/services/smart_ingest.py | 54 +- .../components/final_step_modal.html | 366 ++++++++++ .../components/workspace_actions.html | 11 + app/templates/docs.html | 2 +- app/templates/generate.html | 625 ++++++++++++++++++ app/templates/index.html | 3 + app/templates/landing.html | 3 +- app/templates/select.html | 84 ++- requirements.txt | 3 +- 15 files changed, 1394 insertions(+), 64 deletions(-) create mode 100644 app/routes/generate.py create mode 100644 app/templates/components/final_step_modal.html create mode 100644 app/templates/generate.html diff --git a/.gitignore b/.gitignore index 4cbe0b1..a8ef0de 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,6 @@ __pycache__/ # C extensions *.so -TASK.md -TASK2.md # Distribution / packaging .Python build/ @@ -208,4 +206,9 @@ marimo/_lsp/ __marimo__/ .playwright-mcp -.mcp.json \ No newline at end of file +.mcp.json + +TASK.md +TASK2.md +TASK_IN_PROGRESS.md +TASK_DONE.md diff --git a/app/actions/rules.yaml b/app/actions/rules.yaml index d8cc8d4..00e3617 100644 --- a/app/actions/rules.yaml +++ b/app/actions/rules.yaml @@ -70,7 +70,6 @@ clean-comments: namespace: "development" content: | ## Clean Comments - - When updating code, don't reference what is changing - Avoid keywords like LEGACY, CHANGED, REMOVED - Focus on comments that document just the functionality of the code diff --git a/app/main.py b/app/main.py index ecd078c..3e0f3d7 100644 --- a/app/main.py +++ b/app/main.py @@ -3,16 +3,33 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from pathlib import Path -from app.routes import install, actions, recommend +from app.routes import install, actions, recommend, generate from app.services.actions_loader import actions_loader from api_analytics.fastapi import Analytics from fastapi_mcp import FastApiMCP import os from dotenv import load_dotenv +from loguru import logger +import sys # Load environment variables load_dotenv() +# Configure loguru logger +logger.remove() +logger.add( + sys.stderr, + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} - {message}", + level="INFO" +) +logger.add( + "logs/app.log", + rotation="10 MB", + retention="7 days", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{function}:{line} - {message}", + level="DEBUG" +) + app = FastAPI(title="Gitrules", version="0.1.0") # Add API Analytics middleware @@ -29,6 +46,7 @@ app.include_router(install.router) app.include_router(actions.router) app.include_router(recommend.router) +app.include_router(generate.router) @app.get("/favicon.ico", operation_id="get_favicon") async def favicon(): @@ -44,6 +62,11 @@ async def select(request: Request): """Action selection page with filters""" return templates.TemplateResponse("select.html", {"request": request}) +@app.get("/generate", response_class=HTMLResponse, operation_id="get_generate_page") +async def get_generate_page(request: Request): + """Generate configuration files from selected actions""" + return templates.TemplateResponse("generate.html", {"request": request}) + @app.get("/", response_class=HTMLResponse, operation_id="get_index_page") async def index(request: Request): """Landing page for starting the configuration journey""" diff --git a/app/routes/generate.py b/app/routes/generate.py new file mode 100644 index 0000000..84bee54 --- /dev/null +++ b/app/routes/generate.py @@ -0,0 +1,162 @@ +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from typing import List, Dict, Any, Optional +import json +from app.services.actions_loader import actions_loader + +router = APIRouter(prefix="/api/v2", tags=["generate"]) + +class GenerateRequest(BaseModel): + action_ids: List[str] + formats: List[str] = ["claude"] # claude, cursor, agents + source: str = "scratch" # "repo", "template", or "scratch" + repo_url: Optional[str] = None # For tracking the source repo when source="repo" + +class GenerateResponse(BaseModel): + files: Dict[str, str] + patch: str + source: str + +@router.post("/generate", operation_id="generate_configuration") +async def generate_configuration(request: GenerateRequest) -> GenerateResponse: + """Generate configuration files from selected action IDs""" + + files = {} + + # Load action details + selected_agents = [] + selected_rules = [] + selected_mcps = [] + + for action_id in request.action_ids: + # Try to find the action in different categories + + # Check agents + agent = actions_loader.get_agent(action_id) + if agent: + selected_agents.append(agent) + continue + + # Check rules + rule = actions_loader.get_rule(action_id) + if rule: + selected_rules.append(rule) + continue + + # Check MCPs + mcp = actions_loader.get_mcp(action_id) + if mcp: + selected_mcps.append(mcp) + continue + + # Generate files based on selected formats + for format_type in request.formats: + if format_type == "claude": + # Generate CLAUDE.md if there are rules + if selected_rules: + claude_content = "" + for rule in selected_rules: + if rule.get('content'): + claude_content += rule['content'].strip() + "\n\n" + + if claude_content: + files['CLAUDE.md'] = claude_content.strip() + + # Generate agent files for Claude format + for agent in selected_agents: + if agent.get('content'): + filename = agent.get('filename', f"{agent['name']}.md") + files[f".claude/agents/{filename}"] = agent['content'] + + elif format_type == "cursor": + # Generate .cursorrules file + if selected_rules: + cursor_content = "" + for rule in selected_rules: + if rule.get('content'): + cursor_content += rule['content'].strip() + "\n\n" + + if cursor_content: + files['.cursorrules'] = cursor_content.strip() + + elif format_type == "agents": + # Generate AGENTS.md file with rules (copy of CLAUDE.md) + if selected_rules: + agents_content = "" + for rule in selected_rules: + if rule.get('content'): + agents_content += rule['content'].strip() + "\n\n" + + if agents_content: + files['AGENTS.md'] = agents_content.strip() + + # Generate .mcp.json if there are MCPs + if selected_mcps: + mcp_config = {"mcpServers": {}} + for mcp in selected_mcps: + if mcp.get('config'): + mcp_config["mcpServers"][mcp['name']] = mcp['config'] + + if mcp_config["mcpServers"]: + files['.mcp.json'] = json.dumps(mcp_config, indent=2) + + # Generate patch file + patch = generate_patch(files, request.source, request.repo_url) + + return GenerateResponse(files=files, patch=patch, source=request.source) + +def generate_patch(files: Dict[str, str], source: str = "scratch", repo_url: str = None) -> str: + """ + Generate a unified diff patch from the files. + + Args: + files: Dictionary of file paths and their contents + source: Source of the generation ("repo", "template", or "scratch") + repo_url: URL of source repository if source is "repo" + + Returns: + Unified diff patch string that can be applied with patch command + """ + patch_lines = [] + + # Add a comment header explaining the patch + if source == "repo" and repo_url: + patch_lines.append(f"# Gitrules configuration patch generated from repository: {repo_url}") + patch_lines.append("# Apply with: git apply ") + use_git_format = True + elif source == "template": + patch_lines.append("# Gitrules configuration patch generated from template") + patch_lines.append("# Apply with: patch -p0 < ") + use_git_format = False + else: + patch_lines.append("# Gitrules configuration patch generated from scratch") + patch_lines.append("# Apply with: patch -p0 < ") + use_git_format = False + + patch_lines.append("") + + for filepath, content in files.items(): + if use_git_format: + # Git format + patch_lines.append(f"diff --git a/{filepath} b/{filepath}") + patch_lines.append("new file mode 100644") + patch_lines.append("index 0000000..1234567") + patch_lines.append("--- /dev/null") + patch_lines.append(f"+++ b/{filepath}") + else: + # Standard patch format + patch_lines.append(f"--- /dev/null") + patch_lines.append(f"+++ {filepath}") + + lines = content.split('\n') + if lines and lines[-1] == '': + lines.pop() # Remove empty last line if present + + patch_lines.append(f"@@ -0,0 +1,{len(lines)} @@") + + for line in lines: + patch_lines.append(f"+{line}") + + patch_lines.append("") # Empty line between files + + return '\n'.join(patch_lines) \ No newline at end of file diff --git a/app/routes/recommend.py b/app/routes/recommend.py index fc08fb5..c39e932 100644 --- a/app/routes/recommend.py +++ b/app/routes/recommend.py @@ -13,6 +13,7 @@ call_llm_for_reco, parse_and_validate ) +from loguru import logger router = APIRouter(prefix="/api", tags=["recommend"]) @@ -53,17 +54,17 @@ async def recommend_tools(request: RecommendRequest): status_code=400, detail="Either repo_url or context must be provided" ) - print(f"Getting context for {request.repo_url}") + logger.info(f"Getting context for {request.repo_url}") # Step 1: Get context (ingest if needed) if request.context: - print(f"Using provided context") + logger.info("Using provided context") context = request.context else: # Ingest the repository - print(f"Ingesting repository {request.repo_url}") + logger.info(f"Ingesting repository {request.repo_url}") context = await use_gitingest(request.repo_url) context_size = len(context) - print(f"Context size: {context_size}") + logger.info(f"Context size: {context_size}") # Step 2: Build catalog catalog = build_tools_catalog() diff --git a/app/services/actions_loader.py b/app/services/actions_loader.py index 20fb110..edc202b 100644 --- a/app/services/actions_loader.py +++ b/app/services/actions_loader.py @@ -2,6 +2,7 @@ from typing import List, Dict, Any, Optional from pathlib import Path from app.models.actions import Agent, Rule, MCP, Pack, Action, ActionType +from loguru import logger class ActionsLoader: def __init__(self): @@ -12,6 +13,7 @@ def __init__(self): self.rules: List[Rule] = [] self.mcps: List[MCP] = [] self.packs: List[Pack] = [] + logger.info(f"Loading actions from {self.actions_dir}") self.load_all() def load_all(self): @@ -25,33 +27,39 @@ def load_agents(self): """Load all agents from agents.yaml""" agents_file = self.actions_dir / "agents.yaml" if agents_file.exists(): - with open(agents_file, 'r') as f: - data = yaml.safe_load(f) - if data and 'agents' in data: - for agent_data in data['agents']: - slug = agent_data.get('slug', '') - # Create Action object - action = Action( - id=slug, - name=slug, - display_name=agent_data.get('display_name'), - action_type=ActionType.AGENT, - tags=agent_data.get('tags', []), - content=agent_data.get('content'), - filename=f"{slug}.yaml" - ) - self.actions.append(action) - - # Also create legacy Agent for backward compatibility - self.agents.append(Agent( - name=slug, - filename=f"{slug}.yaml", - display_name=agent_data.get('display_name'), - slug=slug, - content=agent_data.get('content'), - tags=agent_data.get('tags', []) - )) + try: + with open(agents_file, 'r') as f: + data = yaml.safe_load(f) + if data and 'agents' in data: + logger.info(f"Loading {len(data['agents'])} agents") + for agent_data in data['agents']: + slug = agent_data.get('slug', '') + # Create Action object + action = Action( + id=slug, + name=slug, + display_name=agent_data.get('display_name'), + action_type=ActionType.AGENT, + tags=agent_data.get('tags', []), + content=agent_data.get('content'), + filename=f"{slug}.md" + ) + self.actions.append(action) + + # Also create legacy Agent for backward compatibility + self.agents.append(Agent( + name=slug, + filename=f"{slug}.md", + display_name=agent_data.get('display_name'), + slug=slug, + content=agent_data.get('content'), + tags=agent_data.get('tags', []) + )) + except Exception as e: + logger.error(f"Error loading agents from {agents_file}: {e}") + self.agents = [] else: + logger.warning(f"Agents file not found: {agents_file}") self.agents = [] def _parse_rule(self, slug: str, rule_data: Dict[str, Any]) -> Rule: @@ -213,6 +221,47 @@ def get_actions(self, action_type: Optional[ActionType] = None, tags: Optional[L def get_action_by_id(self, action_id: str) -> Optional[Action]: """Get a specific action by ID""" return next((a for a in self.actions if a.id == action_id), None) + + def get_agent(self, action_id: str) -> Optional[Dict[str, Any]]: + """Get agent data by ID for legacy compatibility""" + action = self.get_action_by_id(action_id) + if action and action.action_type == ActionType.AGENT: + return { + 'name': action.name, + 'display_name': action.display_name, + 'slug': action.id, + 'filename': action.filename, + 'content': action.content, + 'tags': action.tags + } + return None + + def get_rule(self, action_id: str) -> Optional[Dict[str, Any]]: + """Get rule data by ID for legacy compatibility""" + action = self.get_action_by_id(action_id) + if action and action.action_type in [ActionType.RULE, ActionType.RULESET]: + return { + 'name': action.name, + 'display_name': action.display_name, + 'slug': action.id, + 'content': action.content, + 'tags': action.tags, + 'type': action.action_type.value.lower() + } + return None + + def get_mcp(self, action_id: str) -> Optional[Dict[str, Any]]: + """Get MCP data by ID for legacy compatibility""" + action = self.get_action_by_id(action_id) + if action and action.action_type == ActionType.MCP: + return { + 'name': action.name, + 'display_name': action.display_name, + 'slug': action.id, + 'config': action.config, + 'tags': action.tags + } + return None # Create singleton instance actions_loader = ActionsLoader() \ No newline at end of file diff --git a/app/services/smart_ingest.py b/app/services/smart_ingest.py index 64cffaf..a647309 100644 --- a/app/services/smart_ingest.py +++ b/app/services/smart_ingest.py @@ -6,7 +6,7 @@ from typing import Optional, Dict, Any from dotenv import load_dotenv import os -from gitingest import ingest_async +from loguru import logger # Load environment variables from .env file load_dotenv() @@ -14,7 +14,7 @@ async def use_gitingest(url: str, context_size: int = 50000) -> str: """ - Ingest a repository using gitingest and trim to specified token size. + Ingest a repository using gitingest.com API and trim to specified token size. Args: url: Repository URL to ingest @@ -23,24 +23,52 @@ async def use_gitingest(url: str, context_size: int = 50000) -> str: Returns: String containing the repository context, trimmed to specified size """ - # Ingest the repository - summary, tree, content = await ingest_async( - url, - max_file_size=512000, - include_patterns=None, - exclude_patterns=None - ) - - # Combine into single context - full_context = f"{summary}\n\n{tree}\n\n{content}" + logger.info(f"Ingesting repository from {url}") + # Query gitingest.com API instead of local package + async with httpx.AsyncClient(timeout=120.0) as client: + try: + # Call gitingest.com API + response = await client.post( + "https://gitingest.com/api/ingest", + json={ + "input_text": url, + "max_file_size": 102400, + "pattern_type": "exclude", + "pattern": "", + "token": "" + }, + headers={ + "Content-Type": "application/json" + } + ) + response.raise_for_status() + + # Parse response - assuming it returns the full context + data = response.json() + full_context = data.get("content", "") + + # If the API returns structured data, combine it + if isinstance(data, dict) and "summary" in data: + summary = data.get("summary", "") + tree = data.get("tree", "") + content = data.get("content", "") + full_context = f"{summary}\n\n{tree}\n\n{content}" + + except httpx.HTTPError as e: + logger.error(f"Failed to ingest repository from gitingest.com: {str(e)}") + raise Exception(f"Failed to ingest repository from gitingest.com: {str(e)}") # Approximate token count (roughly 4 chars per token) # Trim to specified context size max_chars = context_size * 4 + original_length = len(full_context) if len(full_context) > max_chars: full_context = full_context[:max_chars] # Add ellipsis to indicate truncation full_context += "\n\n... (context truncated)" + logger.info(f"Context truncated from {original_length} to {len(full_context)} characters") + else: + logger.info(f"Repository context ingested: {len(full_context)} characters") return full_context @@ -133,6 +161,8 @@ def smart_ingest( except httpx.HTTPStatusError as e: error_detail = e.response.text if e.response else str(e) + logger.error(f"OpenAI API error: {e.response.status_code} - {error_detail}") raise Exception(f"OpenAI API error: {e.response.status_code} - {error_detail}") except Exception as e: + logger.error(f"Failed to send context to OpenAI: {str(e)}") raise Exception(f"Failed to send context to OpenAI: {str(e)}") \ No newline at end of file diff --git a/app/templates/components/final_step_modal.html b/app/templates/components/final_step_modal.html new file mode 100644 index 0000000..bda80d3 --- /dev/null +++ b/app/templates/components/final_step_modal.html @@ -0,0 +1,366 @@ + + + + \ No newline at end of file diff --git a/app/templates/components/workspace_actions.html b/app/templates/components/workspace_actions.html index 4bab1d7..1cd9a75 100644 --- a/app/templates/components/workspace_actions.html +++ b/app/templates/components/workspace_actions.html @@ -117,6 +117,17 @@

Actions

+ + +
+ +
diff --git a/app/templates/docs.html b/app/templates/docs.html index 29f330c..79db153 100644 --- a/app/templates/docs.html +++ b/app/templates/docs.html @@ -120,7 +120,7 @@

- .claude/agents/*.yaml - Specialized AI helpers + .claude/agents/*.md - Specialized AI helpers

Give Claude different "modes" like researcher, reviewer, or debugger.

diff --git a/app/templates/generate.html b/app/templates/generate.html new file mode 100644 index 0000000..8a265d1 --- /dev/null +++ b/app/templates/generate.html @@ -0,0 +1,625 @@ +{% extends "base.html" %} + +{% block content %} + + + + +
+ +
+
+
+
+

Generate Your Configuration

+

Review and download your customized setup

+
+ +
+
+
+ +
+
+ +
+
+

📋 Selected Tools

+
+ +
+ + +
+

🎯 Output Formats

+
+ + + +
+
+
+
+ + +
+ +
+
+
+
+ +
+ +
+
+
+ +
+
+ + +
+
+
+
+

🔧 Configuration Patch

+

Apply this patch to your repository:

+ patch -p0 < gitrules-config.patch +
+ +
+
+
+ +
+
+ + +
+ + +
+
+
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index 8581644..64c3a29 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -14,4 +14,7 @@ {% include 'components/context_modal.html' %} + + + {% include 'components/final_step_modal.html' %} {% endblock %} \ No newline at end of file diff --git a/app/templates/landing.html b/app/templates/landing.html index a3e3a1a..e9367e9 100644 --- a/app/templates/landing.html +++ b/app/templates/landing.html @@ -184,8 +184,9 @@

Start Fresh

const recommendData = await recommendResponse.json(); if (recommendResponse.ok && recommendData.success) { - // Store recommendations and auto-proceed to next page + // Store recommendations and repo URL for later use repositoryRecommendations = recommendData.preselect; + sessionStorage.setItem('lastAnalyzedRepoUrl', repoUrl); // Show brief confirmation then redirect const totalTools = repositoryRecommendations.rules.length + repositoryRecommendations.agents.length + repositoryRecommendations.mcps.length; diff --git a/app/templates/select.html b/app/templates/select.html index b1b25a4..414c48e 100644 --- a/app/templates/select.html +++ b/app/templates/select.html @@ -450,26 +450,82 @@

Includes

function toggleQuickSelection(actionId) { if (selectedActions.has(actionId)) { - selectedActions.delete(actionId); + removeActionFromSelection(actionId); } else { - selectedActions.add(actionId); + addActionToSelection(actionId); } updateSelectedCount(); renderActions(); // Re-render to update selection state } -function toggleActionSelection() { - if (!currentActionId) return; +function addActionToSelection(actionId) { + const action = allActions.find(a => a.id === actionId); + if (!action) return; - if (selectedActions.has(currentActionId)) { - selectedActions.delete(currentActionId); - } else { - selectedActions.add(currentActionId); + selectedActions.add(actionId); + + // If it's a ruleset, select all contained rules + if (action.action_type === 'ruleset' && action.children) { + action.children.forEach(childId => { + selectedActions.add(childId); + }); } - updateSelectedCount(); - renderActions(); // Re-render to update selection state + // If it's a rule, check if we should select parent rulesets + if (action.action_type === 'rule') { + handleRuleSelection(actionId); + } +} + +function removeActionFromSelection(actionId) { + const action = allActions.find(a => a.id === actionId); + if (!action) return; + + selectedActions.delete(actionId); + + // If it's a rule, check if we need to deselect parent rulesets + if (action.action_type === 'rule') { + handleRuleDeselection(actionId); + } + + // If it's a ruleset, deselect all contained rules + if (action.action_type === 'ruleset' && action.children) { + action.children.forEach(childId => { + selectedActions.delete(childId); + }); + } +} + +function handleRuleSelection(ruleId) { + // Find all rulesets that contain this rule + const parentRulesets = allActions.filter(a => + a.action_type === 'ruleset' && a.children && a.children.includes(ruleId) + ); + + // For each parent ruleset, check if all its children are now selected + parentRulesets.forEach(ruleset => { + if (ruleset.children && ruleset.children.every(childId => selectedActions.has(childId))) { + // All children are selected, so select the ruleset too + selectedActions.add(ruleset.id); + } + }); +} + +function handleRuleDeselection(ruleId) { + // Find all rulesets that contain this rule and deselect them + allActions + .filter(a => a.action_type === 'ruleset' && a.children && a.children.includes(ruleId)) + .forEach(ruleset => { + selectedActions.delete(ruleset.id); + }); +} + +function toggleActionSelection() { + if (!currentActionId) return; + + // Use the same logic as toggleQuickSelection + toggleQuickSelection(currentActionId); closeModal(); } @@ -555,7 +611,7 @@

Includes

checkLoaded(); }); - // Pre-select recommended actions + // Pre-select recommended actions using centralized logic const recommendedIds = [ ...(recommendations.agents || []), ...(recommendations.rules || []), @@ -563,7 +619,7 @@

Includes

]; recommendedIds.forEach(id => { - selectedActions.add(id); + addActionToSelection(id); }); updateSelectedCount(); @@ -595,8 +651,8 @@

Includes

// Navigate after fade-out completes setTimeout(() => { - // TODO: Navigate to the next step - alert('Next step will be implemented soon. Selected actions: ' + Array.from(selectedActions).join(', ')); + // Navigate to the generate page with selected actions + window.location.href = `/generate?actions=${Array.from(selectedActions).join(',')}`; }, 300); // Match the fade-out duration } diff --git a/requirements.txt b/requirements.txt index a2b8540..1f7631e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ fastapi-mcp==0.4.0 fuzzywuzzy python-Levenshtein gitingest -httpx \ No newline at end of file +httpx +loguru==0.7.2 \ No newline at end of file From cae1d2a32752f53b684a0a2472fe7e290625ff75 Mon Sep 17 00:00:00 2001 From: Nicolas Iragne Date: Sun, 31 Aug 2025 21:28:59 +0200 Subject: [PATCH 3/5] feat: add descriptions to MCPs and update author information in rules --- app/actions/mcps.yaml | 7 ++ app/actions/rules.yaml | 86 +++++++++++------------ app/models/actions.py | 2 + app/services/actions_loader.py | 10 ++- app/templates/landing.html | 25 +++---- app/templates/select.html | 120 ++++++++++++++++++++++++++++++--- 6 files changed, 179 insertions(+), 71 deletions(-) diff --git a/app/actions/mcps.yaml b/app/actions/mcps.yaml index 8113829..09f4467 100644 --- a/app/actions/mcps.yaml +++ b/app/actions/mcps.yaml @@ -1,6 +1,7 @@ mcps: - display_name: Github slug: GitHub + description: GitHub API integration for repositories, issues, and pull requests config: type: http url: https://api.githubcopilot.com/mcp @@ -8,11 +9,13 @@ mcps: Authorization: Bearer ${GITHUB_TOKEN} - display_name: Firecrawl slug: Firecrawl + description: Web scraping and content extraction from websites config: type: sse url: https://mcp.firecrawl.dev/${FIRECRAWL_API_KEY}/sse - display_name: Playwright slug: Playwright + description: Browser automation and web testing framework config: type: stdio command: npx @@ -20,6 +23,7 @@ mcps: - '@playwright/mcp@latest' - display_name: Supabase slug: Supabase + description: Backend-as-a-service with database and authentication config: command: npx args: @@ -29,6 +33,7 @@ mcps: - ${SUPABASE_ACCESS_TOKEN} - display_name: Context7 slug: Context7 + description: AI-powered context understanding and processing config: type: http url: https://mcp.context7.com/mcp/ @@ -36,11 +41,13 @@ mcps: "CONTEXT7_API_KEY": "${CONTEXT7_API_KEY}" - display_name: Exa Search slug: ExaSearch + description: Advanced search and information retrieval config: type: http url: https://mcp.exa.ai/mcp?exa_api_key=${EXA_API_KEY} - display_name: GitRules slug: GitRules + description: Git workflow automation and rule enforcement config: type: http url: https://gitrules.com/mcp diff --git a/app/actions/rules.yaml b/app/actions/rules.yaml index 00e3617..20c52bd 100644 --- a/app/actions/rules.yaml +++ b/app/actions/rules.yaml @@ -1,7 +1,7 @@ you-are-a-pirate: display_name: You Are A Pirate type: rule - author: Captain Hook + author: Coderamp tags: ["fun", "roleplay", "pirate"] namespace: "personality" content: | @@ -17,7 +17,7 @@ you-are-a-pirate: code-quality: display_name: Code Quality type: ruleset - author: Engineering Team + author: Coderamp tags: ["engineering", "best-practices", "code-style"] namespace: "development" children: @@ -29,7 +29,7 @@ code-quality: minimal-code: display_name: Keep Code Minimal type: rule - author: Engineering Team + author: Coderamp tags: ["simplicity", "code-style"] namespace: "development" content: | @@ -41,7 +41,7 @@ minimal-code: remove-dead-code: display_name: Remove Dead Code type: rule - author: Engineering Team + author: Coderamp tags: ["maintenance", "code-style"] namespace: "development" content: | @@ -53,7 +53,7 @@ remove-dead-code: prioritize-functionality: display_name: Prioritize Functionality type: rule - author: Engineering Team + author: Coderamp tags: ["architecture", "code-style"] namespace: "development" content: | @@ -65,7 +65,7 @@ prioritize-functionality: clean-comments: display_name: Clean Comments type: rule - author: Engineering Team + author: Coderamp tags: ["documentation", "code-style"] namespace: "development" content: | @@ -77,7 +77,7 @@ clean-comments: env-secrets: display_name: Environment Variables type: rule - author: Security Team + author: Coderamp tags: ["security", "configuration", "environment"] namespace: "security" content: | @@ -90,7 +90,7 @@ env-secrets: error-handling: display_name: Error Handling type: ruleset - author: Engineering Team + author: Coderamp tags: ["errors", "exceptions", "reliability"] namespace: "development" children: @@ -101,7 +101,7 @@ error-handling: fail-fast-principle: display_name: Fail Fast Principle type: rule - author: Engineering Team + author: Coderamp tags: ["architecture", "errors"] namespace: "development" content: | @@ -112,7 +112,7 @@ fail-fast-principle: when-to-fail-fast: display_name: When to Fail Fast and Loud type: rule - author: Engineering Team + author: Coderamp tags: ["exceptions", "errors"] namespace: "development" content: | @@ -131,7 +131,7 @@ when-to-fail-fast: when-to-log-continue: display_name: When to Complete but Log type: rule - author: Engineering Team + author: Coderamp tags: ["logging", "errors"] namespace: "development" content: | @@ -144,7 +144,7 @@ when-to-log-continue: update-docs: display_name: Update Documentation type: rule - author: Documentation Team + author: Coderamp tags: ["documentation", "maintenance"] namespace: "documentation" content: | @@ -155,7 +155,7 @@ update-docs: use-uv: display_name: Use UV Package Manager type: rule - author: DevOps Team + author: Coderamp tags: ["tooling", "dependencies", "uv"] namespace: "development" content: | @@ -169,7 +169,7 @@ use-uv: test-driven-development: display_name: Test-Driven Development type: rule - author: QA Team + author: Coderamp tags: ["testing", "tdd", "quality"] namespace: "development" content: | @@ -184,7 +184,7 @@ test-driven-development: api-design: display_name: API Design Standards type: ruleset - author: API Team + author: Coderamp tags: ["api", "rest", "standards"] namespace: "development" children: @@ -196,7 +196,7 @@ api-design: restful-conventions: display_name: RESTful Conventions type: rule - author: API Team + author: Coderamp tags: ["rest", "http", "api"] namespace: "development" content: | @@ -210,7 +210,7 @@ restful-conventions: api-versioning: display_name: API Versioning type: rule - author: API Team + author: Coderamp tags: ["versioning", "api", "compatibility"] namespace: "development" content: | @@ -224,7 +224,7 @@ api-versioning: response-formats: display_name: API Response Formats type: rule - author: API Team + author: Coderamp tags: ["json", "api", "responses"] namespace: "development" content: | @@ -238,7 +238,7 @@ response-formats: rate-limiting: display_name: Rate Limiting type: rule - author: API Team + author: Coderamp tags: ["security", "api", "performance"] namespace: "development" content: | @@ -252,7 +252,7 @@ rate-limiting: database-optimization: display_name: Database Optimization type: ruleset - author: Database Team + author: Coderamp tags: ["database", "performance", "optimization"] namespace: "development" children: @@ -263,7 +263,7 @@ database-optimization: query-optimization: display_name: Query Optimization type: rule - author: Database Team + author: Coderamp tags: ["sql", "performance", "queries"] namespace: "development" content: | @@ -278,7 +278,7 @@ query-optimization: indexing-strategy: display_name: Indexing Strategy type: rule - author: Database Team + author: Coderamp tags: ["indexes", "database", "performance"] namespace: "development" content: | @@ -292,7 +292,7 @@ indexing-strategy: connection-pooling: display_name: Connection Pooling type: rule - author: Database Team + author: Coderamp tags: ["connections", "database", "pooling"] namespace: "development" content: | @@ -306,7 +306,7 @@ connection-pooling: git-workflow: display_name: Git Workflow type: ruleset - author: DevOps Team + author: Coderamp tags: ["git", "version-control", "workflow"] namespace: "development" children: @@ -317,7 +317,7 @@ git-workflow: branch-naming: display_name: Branch Naming Convention type: rule - author: DevOps Team + author: Coderamp tags: ["git", "branches", "naming"] namespace: "development" content: | @@ -332,7 +332,7 @@ branch-naming: commit-conventions: display_name: Commit Message Convention type: rule - author: DevOps Team + author: Coderamp tags: ["git", "commits", "conventions"] namespace: "development" content: | @@ -347,7 +347,7 @@ commit-conventions: pull-request-rules: display_name: Pull Request Rules type: rule - author: DevOps Team + author: Coderamp tags: ["git", "pull-requests", "review"] namespace: "development" content: | @@ -362,7 +362,7 @@ pull-request-rules: security-practices: display_name: Security Best Practices type: ruleset - author: Security Team + author: Coderamp tags: ["security", "best-practices", "safety"] namespace: "security" children: @@ -374,7 +374,7 @@ security-practices: input-validation: display_name: Input Validation type: rule - author: Security Team + author: Coderamp tags: ["validation", "security", "input"] namespace: "security" content: | @@ -389,7 +389,7 @@ input-validation: authentication-rules: display_name: Authentication Rules type: rule - author: Security Team + author: Coderamp tags: ["auth", "security", "authentication"] namespace: "security" content: | @@ -404,7 +404,7 @@ authentication-rules: data-encryption: display_name: Data Encryption type: rule - author: Security Team + author: Coderamp tags: ["encryption", "security", "data"] namespace: "security" content: | @@ -419,7 +419,7 @@ data-encryption: security-headers: display_name: Security Headers type: rule - author: Security Team + author: Coderamp tags: ["headers", "security", "http"] namespace: "security" content: | @@ -434,7 +434,7 @@ security-headers: performance-monitoring: display_name: Performance Monitoring type: ruleset - author: DevOps Team + author: Coderamp tags: ["monitoring", "performance", "observability"] namespace: "operations" children: @@ -445,7 +445,7 @@ performance-monitoring: application-metrics: display_name: Application Metrics type: rule - author: DevOps Team + author: Coderamp tags: ["metrics", "monitoring", "apm"] namespace: "operations" content: | @@ -460,7 +460,7 @@ application-metrics: logging-standards: display_name: Logging Standards type: rule - author: DevOps Team + author: Coderamp tags: ["logging", "observability", "debugging"] namespace: "operations" content: | @@ -475,7 +475,7 @@ logging-standards: alerting-rules: display_name: Alerting Rules type: rule - author: DevOps Team + author: Coderamp tags: ["alerts", "monitoring", "incidents"] namespace: "operations" content: | @@ -490,7 +490,7 @@ alerting-rules: accessibility-standards: display_name: Accessibility Standards type: rule - author: UX Team + author: Coderamp tags: ["a11y", "accessibility", "wcag"] namespace: "frontend" content: | @@ -506,7 +506,7 @@ accessibility-standards: code-review-checklist: display_name: Code Review Checklist type: rule - author: Engineering Team + author: Coderamp tags: ["review", "quality", "checklist"] namespace: "development" content: | @@ -523,7 +523,7 @@ code-review-checklist: async-programming: display_name: Async Programming type: rule - author: Engineering Team + author: Coderamp tags: ["async", "concurrency", "performance"] namespace: "development" content: | @@ -538,7 +538,7 @@ async-programming: containerization: display_name: Containerization type: rule - author: DevOps Team + author: Coderamp tags: ["docker", "containers", "deployment"] namespace: "operations" content: | @@ -554,7 +554,7 @@ containerization: caching-strategy: display_name: Caching Strategy type: rule - author: Engineering Team + author: Coderamp tags: ["cache", "performance", "optimization"] namespace: "development" content: | @@ -570,7 +570,7 @@ caching-strategy: dependency-management: display_name: Dependency Management type: rule - author: Engineering Team + author: Coderamp tags: ["dependencies", "packages", "security"] namespace: "development" content: | @@ -585,7 +585,7 @@ dependency-management: feature-flags: display_name: Feature Flags type: rule - author: Product Team + author: Coderamp tags: ["features", "deployment", "testing"] namespace: "development" content: | diff --git a/app/models/actions.py b/app/models/actions.py index 1d89e39..59fa28e 100644 --- a/app/models/actions.py +++ b/app/models/actions.py @@ -22,6 +22,7 @@ class Action(BaseModel): children: Optional[List[str]] = None # For rulesets and packs filename: Optional[str] = None # For agents/rules namespace: Optional[str] = None # For rules + description: Optional[str] = None # For MCPs, packs, etc. class Agent(BaseModel): name: str # For backward compatibility @@ -47,6 +48,7 @@ class MCP(BaseModel): name: str config: Dict[str, Any] # JSON configuration from mcps.json tags: Optional[List[str]] = None + description: Optional[str] = None class Pack(BaseModel): """A pack is a collection of other actions""" diff --git a/app/services/actions_loader.py b/app/services/actions_loader.py index edc202b..65e3c2a 100644 --- a/app/services/actions_loader.py +++ b/app/services/actions_loader.py @@ -123,9 +123,11 @@ def load_mcps(self): action = Action( id=name, name=name, + display_name=mcp_data.get('display_name'), action_type=ActionType.MCP, tags=mcp_data.get('tags', []), - config=mcp_data.get('config', {}) + config=mcp_data.get('config', {}), + description=mcp_data.get('description') ) self.actions.append(action) @@ -133,7 +135,8 @@ def load_mcps(self): self.mcps.append(MCP( name=name, config=mcp_data.get('config', {}), - tags=mcp_data.get('tags', []) + tags=mcp_data.get('tags', []), + description=mcp_data.get('description') )) else: self.mcps = [] @@ -259,7 +262,8 @@ def get_mcp(self, action_id: str) -> Optional[Dict[str, Any]]: 'display_name': action.display_name, 'slug': action.id, 'config': action.config, - 'tags': action.tags + 'tags': action.tags, + 'description': action.description } return None diff --git a/app/templates/landing.html b/app/templates/landing.html index e9367e9..6b7baf7 100644 --- a/app/templates/landing.html +++ b/app/templates/landing.html @@ -65,14 +65,15 @@

Error:

-
+
-
+
📋
-

Use Template

-

Start with a pre-configured template

+

Use Template + COMING SOON +

+

Start with a pre-configured template