diff --git a/.gitignore b/.gitignore index 908fa41..a8ef0de 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,6 @@ __pycache__/ # C extensions *.so -TASK.md -TASK2.md # Distribution / packaging .Python build/ @@ -206,3 +204,11 @@ cython_debug/ marimo/_static/ marimo/_lsp/ __marimo__/ + +.playwright-mcp +.mcp.json + +TASK.md +TASK2.md +TASK_IN_PROGRESS.md +TASK_DONE.md 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/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 d8cc8d4..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,12 +65,11 @@ prioritize-functionality: clean-comments: display_name: Clean Comments type: rule - author: Engineering Team + author: Coderamp tags: ["documentation", "code-style"] 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 @@ -78,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: | @@ -91,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: @@ -102,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: | @@ -113,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: | @@ -132,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: | @@ -145,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: | @@ -156,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: | @@ -170,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: | @@ -185,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: @@ -197,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: | @@ -211,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: | @@ -225,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: | @@ -239,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: | @@ -253,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: @@ -264,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: | @@ -279,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: | @@ -293,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: | @@ -307,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: @@ -318,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: | @@ -333,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: | @@ -348,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: | @@ -363,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: @@ -375,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: | @@ -390,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: | @@ -405,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: | @@ -420,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: | @@ -435,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: @@ -446,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: | @@ -461,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: | @@ -476,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: | @@ -491,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: | @@ -507,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: | @@ -524,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: | @@ -539,7 +538,7 @@ async-programming: containerization: display_name: Containerization type: rule - author: DevOps Team + author: Coderamp tags: ["docker", "containers", "deployment"] namespace: "operations" content: | @@ -555,7 +554,7 @@ containerization: caching-strategy: display_name: Caching Strategy type: rule - author: Engineering Team + author: Coderamp tags: ["cache", "performance", "optimization"] namespace: "development" content: | @@ -571,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: | @@ -586,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/main.py b/app/main.py index c321bea..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(): @@ -39,56 +57,21 @@ 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("/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): - # 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..59fa28e 100644 --- a/app/models/actions.py +++ b/app/models/actions.py @@ -1,5 +1,28 @@ 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 + description: Optional[str] = None # For MCPs, packs, etc. class Agent(BaseModel): name: str # For backward compatibility @@ -7,6 +30,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 +47,24 @@ class Rule(BaseModel): 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""" + 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/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 ae8c45e..65e3c2a 100644 --- a/app/services/actions_loader.py +++ b/app/services/actions_loader.py @@ -1,14 +1,19 @@ 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 +from loguru import logger 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] = [] + logger.info(f"Loading actions from {self.actions_dir}") self.load_all() def load_all(self): @@ -16,25 +21,45 @@ 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""" 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: - 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 in data['agents'] - ] + 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: @@ -66,6 +91,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 +117,27 @@ 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, + display_name=mcp_data.get('display_name'), + action_type=ActionType.MCP, + tags=mcp_data.get('tags', []), + config=mcp_data.get('config', {}), + description=mcp_data.get('description') ) - 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', []), + description=mcp_data.get('description') + )) else: self.mcps = [] @@ -113,6 +168,104 @@ 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) + + 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, + 'description': action.description + } + 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/base.html b/app/templates/base.html index dd5ec72..eaea36a 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -31,9 +31,7 @@ {% include 'components/navbar.html' %}
-
- {% block content %}{% endblock %} -
+ {% block content %}{% endblock %}
{% include 'components/footer.html' %} 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 new file mode 100644 index 0000000..3a48f17 --- /dev/null +++ b/app/templates/landing.html @@ -0,0 +1,226 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

+ Rules for + coding agents +

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

Start from Repository

+

Import settings from an existing repository

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

Use Template + COMING SOON +

+

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..84ebf9f --- /dev/null +++ b/app/templates/select.html @@ -0,0 +1,801 @@ +{% 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 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