Claude Skill

serena

Semantic code understanding with IDE-like symbol operations. Use when: (1) Large codebase analysis (>50 files), (2) Symbol-level operations (find, rename, refactor), (3) Cross-file reference tracking, (4) Project memory and session persistence, (5) Multi-language semantic navigat

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download dianel555-dskills-skills_serena-908af01.zip · 28 KB
Part of dianel555/dskills — 14 skills

Install

skills CLI npx skills add https://github.com/Dianel555/DSkills/tree/main/skills/serena
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dianel555-dskills@llmmart
Git git clone https://github.com/Dianel555/DSkills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole dianel555/dskills collection as a plugin from our marketplace. Git is the plain clone.

README

Serena CLI

Semantic code understanding with IDE-like symbol operations and Web Dashboard. MCP-independent CLI for code navigation, editing, and project memory.

Features

  • Web Dashboard: Real-time configuration monitoring and management
  • Symbol Operations: Find, rename, replace, insert symbols with language-aware precision
  • Cross-file References: Track symbol usages across entire codebase
  • Project Memory: Persist and retrieve project knowledge across sessions
  • Extended Tools: Shell commands, config file operations
  • Cross-Drive Support: Work with projects on any drive (Windows) or path (Unix)
  • Auto Project Registration: Automatically registers projects in global Serena config

Installation

pip install serena-agent typer pyyaml

Quick Start

First-time setup: Launch the Web Dashboard to initialize and register the project:

# Start Web Dashboard (recommended for first-time use)
python -m tools dashboard serve --open-browser

# Or manually open browser after starting
python -m tools dashboard serve
# Then open: http://127.0.0.1:24282/dashboard/index.html

Configuration: Create .env file in skills/serena/ directory:

SERENA_CONTEXT=claude-code
SERENA_MODES=interactive,editing,onboarding
SERENA_PROJECT=.
SERENA_DASHBOARD_ENABLED=true
SERENA_DASHBOARD_PORT=24282

Basic Usage:

# Find a symbol
python -m tools symbol find MyClass --body

# List symbols in file
python -m tools symbol overview src/main.py

# Find references
python -m tools symbol refs MyClass/method

# List available tools
python -m tools workflow tools

Command Structure

python -m tools [GLOBAL OPTIONS] <command> [COMMAND OPTIONS]

Global Options

Global options must be specified before the command:

  • -p, --project PATH - Project directory (default: current directory, env: SERENA_PROJECT)
  • -c, --context TEXT - Execution context (auto-detected if not specified, env: SERENA_CONTEXT)
  • -m, --mode TEXT - Operation modes (can be specified multiple times, env: SERENA_MODES)

Working with Different Projects

Important: When working with projects in different locations (especially cross-drive on Windows), use --project:

# Correct: Specify project path with --project
python -m tools --project "E:\MyProject" file search "pattern"
python -m tools --project "/home/user/project" symbol find MyClass
python -m tools --project "D:\workspace\app" symbol refs MyClass

# Incorrect: Don't use --path with absolute paths from different drives
python -m tools file search "pattern" --path "E:\MyProject"  # Will fail!

Why? The --path option in subcommands expects relative paths within the project. Always use --project to set the project root first.

CLI Commands

Dashboard Commands

Command Description
dashboard serve [--open-browser] [--browser-cmd <path>] Start Web Dashboard server
dashboard info Show current configuration overview
dashboard tools List active and available tools
dashboard modes List active and available modes
dashboard contexts List active and available contexts

Dashboard Options:

  • --open-browser / --no-open-browser: Auto-open browser (default: False)
  • --browser-cmd <path>: Specify browser executable path
  • --host <address>: Listen address (default: 127.0.0.1)
  • --port <number>: Listen port (default: 24282, 0 for auto-select)
  • SERENA_BROWSER_CMD: Environment variable for browser command

Symbol Operations

Command Description
symbol find <name> [--body] [--depth N] [--path file] Find symbols by name
symbol overview <path> List all symbols in file
symbol refs <name> [--path file] Find symbol references
symbol replace <name> --path <file> --body <code> Replace symbol body
symbol insert-after <name> --path <file> --content <code> Insert after symbol
symbol insert-before <name> --path <file> --content <code> Insert before symbol
symbol rename <name> <new> --path <file> Rename symbol

Memory Operations

Command Description
memory list List all memories
memory read <name> Read memory content
memory write <name> --content <text> Create/update memory
memory edit <name> --content <text> Edit memory
memory delete <name> Delete memory

File Operations

Command Description
file list [--path <dir>] [--recursive] List directory contents
file find <pattern> Find files by glob pattern
file search <pattern> [--path <dir>] Search for regex pattern in files

Extended Tools

Command Description
cmd run <cmd> [--cwd <dir>] [--timeout <N>] Execute shell command
cmd script <path> [--args "..."] Execute script file
config read <path> [--format json\|yaml] Read JSON/YAML config
config update <path> <key> <value> Update config value

Workflow

Command Description
workflow onboarding Run project onboarding
workflow check Check onboarding status
workflow tools [--scope all] List available tools

Output Format

All CLI output is JSON:

// Success
{"result": <data>}

// Error
{"error": {"code": "ERROR_CODE", "message": "description"}}

Error codes: INVALID_ARGS, TOOL_NOT_FOUND, INIT_FAILED, RUNTIME_ERROR

Web Dashboard Features

The Web Dashboard provides:

  • Real-time Configuration: View active context, modes, and tools
  • Project Management: See registered projects and active project
  • Tool Monitoring: Track active and available tools
  • Configuration Editing: Edit .env file directly from browser
  • Auto Registration: Automatically adds project to ~/.serena/serena_config.yml

Access the dashboard at: http://127.0.0.1:24282/dashboard/index.html

Project Structure

skills/serena/
├── SKILL.md              
├── README.md             
├── .env.example          
└── tools/
    ├── core.py           # SerenaCore wrapper
    ├── paths.py          # Path utilities
    ├── output.py         # JSON output utilities
    ├── cli/              # Typer CLI commands
    │   ├── dashboard.py  
    │   ├── symbol.py     
    │   ├── memory.py     
    │   ├── file.py       
    │   ├── workflow.py   
    │   ├── cmd.py        
    │   └── config.py     
    ├── server/           # Web Dashboard server
    │   └── dashboard_server.py  # Flask HTTP server
    └── extended/         # Extended tools
        ├── cmd_tools.py
        └── config_tools.py

License

MIT

Skill manifest

Serena - Semantic Code Understanding

IDE-like semantic code operations via CLI. Provides symbol-level code navigation, editing, and project memory.

Prerequisites

pip install serena-agent typer pyyaml

Quick Start

First-time setup: Launch the Web Dashboard to initialize and register the project:

python -m tools dashboard serve --open-browser

This will:

  • Initialize Serena configuration
  • Register the current project in ~/.serena/serena_config.yml
  • Open the Web Dashboard for monitoring and configuration

Configuration: Edit .env file in skills/serena/ directory:

SERENA_CONTEXT=claude-code
SERENA_MODES=interactive,editing,onboarding
SERENA_PROJECT=.

Usage

Basic Command Structure

python -m tools [GLOBAL OPTIONS] <command> [COMMAND OPTIONS]

Global Options (must be specified before the command):

  • -p, --project PATH - Project directory (default: current directory, env: SERENA_PROJECT)
  • -c, --context TEXT - Execution context (auto-detected if not specified, env: SERENA_CONTEXT)
  • -m, --mode TEXT - Operation modes (can be specified multiple times, env: SERENA_MODES)

Working with Different Projects

Important: When working with projects in different locations (especially cross-drive on Windows), use --project:

# Correct: Use --project for different project locations
python -m tools --project "/path/to/project" symbol find MyClass
python -m tools --project "E:\MyProject" file search "pattern"

# Incorrect: Don't use --path with absolute paths from different drives
python -m tools file search "pattern" --path "E:\MyProject"  # Will fail!

The --path option in subcommands expects relative paths within the project. Always use --project to set the project root first.

Common Operations

# Dashboard
python -m tools dashboard serve --open-browser
python -m tools dashboard info

# Symbol operations
python -m tools symbol find MyClass --body
python -m tools symbol overview src/main.py
python -m tools symbol refs MyClass/method
python -m tools symbol rename OldName NewName --path src/file.py

# Memory operations
python -m tools memory list
python -m tools memory read project_overview
python -m tools memory write api_notes --content "..."

# File operations
python -m tools file list --recursive
python -m tools file find "**/*.py"
python -m tools file search "TODO:.*" --path src

# Extended tools
python -m tools cmd run "git status"
python -m tools config read config.json

Tool Routing Policy

Prefer Serena Over Built-in Tools

Task Avoid Use Serena CLI
Find function grep "def func" symbol find func --body
List file structure cat file.py symbol overview file.py
Find usages grep "func(" symbol refs func
Edit function Edit tool symbol replace func --path file.py
Rename Manual find/replace symbol rename old new --path file.py

When to Use Built-in Tools

  • Simple text search (non-code patterns)
  • Configuration files (JSON, YAML)
  • Documentation files (Markdown)

Command Reference

Dashboard Commands

Command Description
dashboard serve [--open-browser] [--browser-cmd <path>] Start Web Dashboard server
dashboard info Show current configuration
dashboard tools List active and available tools
dashboard modes List active and available modes
dashboard contexts List active and available contexts

Symbol Commands

Command Description
symbol find <name> [--body] [--depth N] [--path file] Find symbols by name
symbol overview <path> List all symbols in file
symbol refs <name> [--path file] Find symbol references
symbol replace <name> --path <file> --body <code> Replace symbol body
symbol insert-after <name> --path <file> --content <code> Insert after symbol
symbol insert-before <name> --path <file> --content <code> Insert before symbol
symbol rename <name> <new> --path <file> Rename symbol

Memory Commands

Command Description
memory list List all memories
memory read <name> Read memory content
memory write <name> --content <text> Create/update memory
memory edit <name> --content <text> Edit memory
memory delete <name> Delete memory

File Commands

Command Description
file list [--path dir] [--recursive] List directory
file find <pattern> Find files by glob pattern
file search <pattern> [--path dir] Search for regex pattern

Extended Commands

Command Description
cmd run <command> [--cwd dir] [--timeout N] Execute shell command
cmd script <path> [--args "..."] Execute script file
config read <path> [--format json\|yaml] Read config file
config update <path> <key> <value> Update config value

Workflow Commands

Command Description
workflow onboarding Run project onboarding
workflow check Check onboarding status
workflow tools [--scope all] List available tools

Workflow Examples

Phase 1: Exploration

python -m tools symbol overview src/main.py           # Understand file structure
python -m tools symbol find MyClass --depth 1         # Explore class members
python -m tools symbol find MyClass/method --body     # Get implementation details

Phase 2: Analysis

python -m tools symbol refs MyClass/method            # Impact analysis
python -m tools memory list                           # Check project knowledge
python -m tools memory read architecture              # Retrieve context

Phase 3: Modification

python -m tools symbol find target --body             # Verify target
python -m tools symbol replace target --path f --body "..."  # Edit
python -m tools symbol rename old new --path f        # Refactor

Error Handling

All CLI output is JSON:

// Success
{"result": <data>}

// Error
{"error": {"code": "ERROR_CODE", "message": "description"}}
Error Code Recovery
INVALID_ARGS Check --help
TOOL_NOT_FOUND Use workflow tools
INIT_FAILED Check serena-agent installation
RUNTIME_ERROR Check error message

Anti-Patterns

Prohibited Correct
Read entire file to find function symbol find func --body
Grep for function calls symbol refs func
Manual search-replace rename symbol rename old new --path f
Skip impact analysis symbol refs before editing
Files (dskills)
  • tools
    • cli
      • cmd.py 1 KB
        """Command execution command group."""
        import typer
        from typing import Optional
        from . import State
        from ..output import output_json
        
        app = typer.Typer(help="Command execution operations")
        
        
        @app.command("run")
        def run_command(
            command: str = typer.Argument(..., help="Shell command to execute"),
            cwd: Optional[str] = typer.Option(None, "--cwd", help="Working directory"),
            timeout: int = typer.Option(300, "--timeout", "-t", help="Timeout in seconds"),
        ):
            """Execute a shell command."""
            result = State.core.call_tool(
                "run_command",
                command=command,
                cwd=cwd,
                timeout=timeout,
            )
            output_json(result)
        
        
        @app.command("script")
        def run_script(
            script_path: str = typer.Argument(..., help="Path to script file"),
            args: Optional[str] = typer.Option(None, "--args", "-a", help="Script arguments"),
        ):
            """Execute a script file."""
            result = State.core.call_tool(
                "run_script",
                script_path=script_path,
                args=args,
            )
            output_json(result)
        
      • config.py 1.1 KB
        """Configuration file operations command group."""
        import typer
        from typing import Optional
        from . import State
        from ..output import output_json
        
        app = typer.Typer(help="Configuration file operations")
        
        
        @app.command("read")
        def read_config(
            path: str = typer.Argument(..., help="Path to configuration file"),
            format: Optional[str] = typer.Option(None, "--format", "-f", help="Format: json or yaml (auto-detected if omitted)"),
        ):
            """Read a configuration file."""
            result = State.core.call_tool(
                "read_config",
                path=path,
                format=format,
            )
            output_json(result)
        
        
        @app.command("update")
        def update_config(
            path: str = typer.Argument(..., help="Path to configuration file"),
            key: str = typer.Argument(..., help="Dot-notation key (e.g., 'server.port')"),
            value: str = typer.Argument(..., help="New value (parsed as JSON if possible)"),
        ):
            """Update a configuration file key."""
            result = State.core.call_tool(
                "update_config",
                path=path,
                key=key,
                value=value,
            )
            output_json(result)
        
      • dashboard.py 5.3 KB
        """Dashboard-like information command group."""
        import os
        import shlex
        import subprocess
        import sys
        from typing import Optional
        
        import typer
        from . import State
        from ..output import output_json, output_error
        
        app = typer.Typer(help="Dashboard-style configuration overview")
        
        
        def _resolve_windows_browser_command() -> Optional[list[str]]:
            """Resolve default HTTP browser command from Windows registry."""
            try:
                import winreg
            except Exception:
                return None
        
            try:
                with winreg.OpenKey(
                    winreg.HKEY_CURRENT_USER,
                    r"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice",
                ) as key:
                    prog_id, _ = winreg.QueryValueEx(key, "ProgId")
        
                with winreg.OpenKey(
                    winreg.HKEY_CLASSES_ROOT,
                    rf"{prog_id}\shell\open\command",
                ) as key:
                    command, _ = winreg.QueryValueEx(key, None)
        
                tokens = shlex.split(command, posix=False)
                cleaned: list[str] = []
                for token in tokens:
                    token_lower = token.lower()
                    if token_lower in {"%1", "%l", "%*"}:
                        continue
                    if "%1" in token_lower or "%l" in token_lower:
                        continue
                    cleaned.append(token)
                return cleaned or None
            except Exception:
                return None
        
        
        def _open_browser_url(url: str, browser_cmd: Optional[str] = None) -> str:
            """Open URL in browser using the most reliable method for each platform."""
            if sys.platform == "win32":
                if browser_cmd:
                    cmd = shlex.split(browser_cmd, posix=False)
                    subprocess.Popen([*cmd, url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                    return "explicit-browser-cmd"
                resolved = _resolve_windows_browser_command()
                if resolved:
                    subprocess.Popen([*resolved, url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                    return "windows-registry-command"
                os.startfile(url)
                return "os.startfile-fallback"
            if sys.platform == "darwin":
                subprocess.Popen(["open", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                return "open"
            subprocess.Popen(["xdg-open", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            return "xdg-open"
        
        
        @app.command("info")
        def info():
            """Show current configuration and discoverable options."""
            output_json({"result": State.core.get_dashboard_info()})
        
        
        @app.command("tools")
        def tools():
            """Show active and available tools."""
            active_tools = State.core.list_tools(scope="active")
            available_tools = State.core.list_tools(scope="all")
            output_json(
                {
                    "result": {
                        "active_count": len(active_tools),
                        "active": active_tools,
                        "available_count": len(available_tools),
                        "available": available_tools,
                    }
                }
            )
        
        
        @app.command("modes")
        def modes():
            """Show active and available modes."""
            active_modes = State.core.list_modes(scope="active")
            available_modes = State.core.list_modes(scope="all")
            output_json(
                {"result": {"active": active_modes, "available": available_modes}}
            )
        
        
        @app.command("contexts")
        def contexts():
            """Show active and available contexts."""
            active_contexts = State.core.list_contexts(scope="active")
            available_contexts = State.core.list_contexts(scope="all")
            output_json(
                {"result": {"active": active_contexts, "available": available_contexts}}
            )
        
        
        @app.command("serve")
        def serve(
            host: str = typer.Option(
                "127.0.0.1",
                "--host",
                help="Dashboard listen address",
            ),
            port: int = typer.Option(
                0,
                "--port",
                "-p",
                help="Dashboard listen port (0 means auto-select)",
            ),
            open_browser: bool = typer.Option(
                False,
                "--open-browser/--no-open-browser",
                help="Open dashboard URL in browser on startup",
            ),
            browser_cmd: Optional[str] = typer.Option(
                None,
                "--browser-cmd",
                envvar="SERENA_BROWSER_CMD",
                help="Launch browser via explicit command, e.g. 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'",
            ),
        ):
            """Serve Serena web dashboard."""
            if State.core is None:
                output_error("INIT_FAILED", "SerenaCore is not initialized")
        
            from ..server.dashboard_server import DashboardServer
        
            dashboard_server = DashboardServer(core=State.core)
            thread, actual_port = dashboard_server.run_in_thread(
                host=host, port=port if port > 0 else None
            )
            url = dashboard_server.get_dashboard_url(host=host, port=actual_port)
        
            if open_browser:
                try:
                    _open_browser_url(url, browser_cmd=browser_cmd)
                except Exception as e:
                    print(f"[WARNING] Failed to open browser: {e}", file=sys.stderr)
            else:
                # Display URL for manual opening
                print(f"\n{'='*70}", file=sys.stderr)
                print(f"  Serena Dashboard is running!", file=sys.stderr)
                print(f"  URL: {url}", file=sys.stderr)
                print(f"  Copy and paste this URL into your browser", file=sys.stderr)
                print(f"  Or use --open-browser flag to open automatically", file=sys.stderr)
                print(f"{'='*70}\n", file=sys.stderr)
        
            output_json({"result": {"status": "running", "host": host, "port": actual_port, "url": url}})
            while thread.is_alive():
                thread.join(timeout=0.5)
        
      • file.py 1.4 KB
        """File operations command group."""
        import typer
        from typing import Optional
        from . import State
        from ..output import output_json
        
        app = typer.Typer(help="File system operations (list, find, search)")
        
        
        @app.command("list")
        def list_dir(
            path: Optional[str] = typer.Option(None, "--path", "-p", help="Directory path (default: project root)"),
            recursive: bool = typer.Option(False, "--recursive", "-r", help="List recursively"),
        ):
            """List directory contents."""
            result = State.core.call_tool(
                "list_dir",
                relative_path=path if path is not None else ".",
                recursive=recursive,
            )
            output_json(result)
        
        
        @app.command("find")
        def find_file(
            pattern: str = typer.Argument(..., help="File pattern (glob syntax, e.g., '**/*.py')"),
        ):
            """Find files by pattern."""
            result = State.core.call_tool(
                "find_file",
                file_mask=pattern,
                relative_path=".",
            )
            output_json(result)
        
        
        @app.command("search")
        def search_pattern(
            pattern: str = typer.Argument(..., help="Search pattern (regex)"),
            path: Optional[str] = typer.Option(None, "--path", "-p", help="Restrict search to file/directory"),
        ):
            """Search for pattern in files."""
            result = State.core.call_tool(
                "search_for_pattern",
                substring_pattern=pattern,
                relative_path=path if path is not None else ".",
            )
            output_json(result)
        
      • memory.py 1.5 KB
        """Memory operations command group."""
        import typer
        from . import State
        from ..output import output_json
        
        app = typer.Typer(help="Project memory operations (persistent knowledge)")
        
        
        @app.command("list")
        def list_memories():
            """List all project memories."""
            result = State.core.call_tool("list_memories")
            output_json(result)
        
        
        @app.command("read")
        def read_memory(
            name: str = typer.Argument(..., help="Memory name"),
        ):
            """Read memory content."""
            result = State.core.call_tool(
                "read_memory",
                memory_file_name=name,
            )
            output_json(result)
        
        
        @app.command("write")
        def write_memory(
            name: str = typer.Argument(..., help="Memory name"),
            content: str = typer.Option(..., "--content", "-c", help="Memory content"),
        ):
            """Create or update memory."""
            result = State.core.call_tool(
                "write_memory",
                memory_name=name,
                content=content,
            )
            output_json(result)
        
        
        @app.command("edit")
        def edit_memory(
            name: str = typer.Argument(..., help="Memory name"),
            content: str = typer.Option(..., "--content", "-c", help="New memory content"),
        ):
            """Edit existing memory."""
            result = State.core.call_tool(
                "write_memory",
                memory_name=name,
                content=content,
            )
            output_json(result)
        
        
        @app.command("delete")
        def delete_memory(
            name: str = typer.Argument(..., help="Memory name"),
        ):
            """Delete a memory."""
            result = State.core.call_tool(
                "delete_memory",
                memory_file_name=name,
            )
            output_json(result)
        
      • symbol.py 3.7 KB
        """Symbol operations command group."""
        import typer
        from typing import Optional
        from . import State
        from ..output import output_json
        
        app = typer.Typer(help="Symbol operations (find, overview, references, edit)")
        
        
        @app.command("find")
        def find_symbol(
            name_path: str = typer.Argument(..., help="Symbol name or path pattern"),
            path: Optional[str] = typer.Option(None, "--path", "-p", help="Restrict search to file/directory"),
            body: bool = typer.Option(False, "--body", "-b", help="Include symbol body in output"),
            depth: int = typer.Option(0, "--depth", "-d", help="Depth for nested symbols (0=current level only)"),
            exact: bool = typer.Option(False, "--exact", "-e", help="Exact match (disable substring matching)"),
        ):
            """Find symbols by name or path pattern."""
            result = State.core.call_tool(
                "find_symbol",
                name_path=name_path,
                relative_path=path,
                include_body=body,
                depth=depth,
                substring_matching=not exact,
            )
            output_json(result)
        
        
        @app.command("overview")
        def symbols_overview(
            path: str = typer.Argument(..., help="File path to analyze"),
        ):
            """List all symbols in a file."""
            result = State.core.call_tool(
                "get_symbols_overview",
                relative_path=path,
            )
            output_json(result)
        
        
        @app.command("refs")
        def find_refs(
            name_path: str = typer.Argument(..., help="Symbol name or path"),
            path: Optional[str] = typer.Option(None, "--path", "-p", help="Restrict search to file/directory"),
            snippets: bool = typer.Option(True, "--snippets/--no-snippets", help="Include code snippets"),
        ):
            """Find references to a symbol."""
            result = State.core.call_tool(
                "find_referencing_symbols",
                name_path=name_path,
                relative_path=path,
                include_code_snippets=snippets,
            )
            output_json(result)
        
        
        @app.command("replace")
        def replace_symbol(
            name_path: str = typer.Argument(..., help="Symbol name or path"),
            path: str = typer.Option(..., "--path", "-p", help="File containing the symbol"),
            body: str = typer.Option(..., "--body", "-b", help="New symbol body"),
        ):
            """Replace symbol body."""
            result = State.core.call_tool(
                "replace_symbol_body",
                name_path=name_path,
                relative_path=path,
                new_body=body,
            )
            output_json(result)
        
        
        @app.command("insert-after")
        def insert_after(
            name_path: str = typer.Argument(..., help="Symbol name or path"),
            path: str = typer.Option(..., "--path", "-p", help="File containing the symbol"),
            content: str = typer.Option(..., "--content", "-c", help="Content to insert"),
        ):
            """Insert content after a symbol."""
            result = State.core.call_tool(
                "insert_after_symbol",
                name_path=name_path,
                relative_path=path,
                content=content,
            )
            output_json(result)
        
        
        @app.command("insert-before")
        def insert_before(
            name_path: str = typer.Argument(..., help="Symbol name or path"),
            path: str = typer.Option(..., "--path", "-p", help="File containing the symbol"),
            content: str = typer.Option(..., "--content", "-c", help="Content to insert"),
        ):
            """Insert content before a symbol."""
            result = State.core.call_tool(
                "insert_before_symbol",
                name_path=name_path,
                relative_path=path,
                content=content,
            )
            output_json(result)
        
        
        @app.command("rename")
        def rename_symbol(
            name_path: str = typer.Argument(..., help="Symbol name or path"),
            new_name: str = typer.Argument(..., help="New symbol name"),
            path: str = typer.Option(..., "--path", "-p", help="File containing the symbol"),
        ):
            """Rename a symbol."""
            result = State.core.call_tool(
                "rename_symbol",
                name_path=name_path,
                relative_path=path,
                new_name=new_name,
            )
            output_json(result)
        
      • workflow.py 780 B
        """Workflow operations command group."""
        import typer
        from . import State
        from ..output import output_json
        
        app = typer.Typer(help="Workflow operations (onboarding, diagnostics)")
        
        
        @app.command("onboarding")
        def onboarding():
            """Run project onboarding process."""
            result = State.core.call_tool("onboarding")
            output_json(result)
        
        
        @app.command("check")
        def check_onboarding():
            """Check if onboarding has been performed."""
            result = State.core.call_tool("check_onboarding_performed")
            output_json(result)
        
        
        @app.command("tools")
        def list_tools(
            scope: str = typer.Option("active", "--scope", "-s", help="Tool scope: 'active' or 'all'"),
        ):
            """List available tools."""
            tools = State.core.list_tools(scope=scope)
            output_json({"result": tools})
        
      • __init__.py 3.4 KB
        """CLI package for Serena tools using Typer."""
        import os
        import typer
        from typing import Optional, List
        from pathlib import Path
        
        # Load .env file if it exists
        def _load_dotenv():
            """Load environment variables from .env file."""
            try:
                from dotenv import load_dotenv
                # Try multiple locations
                env_paths = [
                    Path(__file__).parent.parent / ".env",  # skills/serena/tools/.env
                    Path(__file__).parent.parent.parent / ".env",  # skills/serena/.env
                    Path.cwd() / ".env",  # current directory
                ]
                for env_path in env_paths:
                    if env_path.exists():
                        load_dotenv(env_path, override=False)
                        return
            except ImportError:
                # python-dotenv not installed, skip
                pass
        
        _load_dotenv()
        
        app = typer.Typer(
            name="serena",
            help="Semantic code operations with IDE-like symbol navigation",
            no_args_is_help=True,
        )
        
        
        class State:
            """Global state for sharing SerenaCore instance across commands."""
            core: Optional["SerenaCore"] = None
        
        
        @app.callback()
        def main(
            ctx: typer.Context,
            project: str = typer.Option(
                ".",
                "-p",
                "--project",
                envvar="SERENA_PROJECT",
                help="Project directory path",
            ),
            context: Optional[str] = typer.Option(
                None,
                "-c",
                "--context",
                envvar="SERENA_CONTEXT",
                help="Execution context (agent, claude-code, ide, codex). Auto-detected if not specified.",
            ),
            mode: Optional[List[str]] = typer.Option(
                None,
                "-m",
                "--mode",
                help="Operation modes (can be specified multiple times)",
            ),
        ):
            """
            Initialize SerenaCore with global options.
        
            All commands inherit these global options and share the same SerenaCore instance.
            """
            from ..core import SerenaCore
            from ..extended.cmd_tools import RunCommandTool, RunScriptTool
            from ..extended.config_tools import ReadConfigTool, UpdateConfigTool
            from ..output import output_error
        
            try:
                # Initialize SerenaCore with global options
                # Read modes from environment variable if not provided via CLI
                if mode is None:
                    import os
                    env_modes = os.environ.get("SERENA_MODES")
                    if env_modes:
                        modes = [m.strip() for m in env_modes.split(",")]
                    else:
                        modes = ["interactive", "editing"]
                else:
                    modes = mode
                State.core = SerenaCore(project=project, context=context, modes=modes)
        
                # Register extended tools
                State.core.register_tool(RunCommandTool())
                State.core.register_tool(RunScriptTool())
                State.core.register_tool(ReadConfigTool())
                State.core.register_tool(UpdateConfigTool())
        
            except Exception as e:
                output_error("INIT_FAILED", str(e))
        
        
        # Command groups will be registered here
        from .symbol import app as symbol_app
        from .memory import app as memory_app
        from .file import app as file_app
        from .workflow import app as workflow_app
        from .cmd import app as cmd_app
        from .config import app as config_app
        from .dashboard import app as dashboard_app
        
        app.add_typer(symbol_app, name="symbol")
        app.add_typer(memory_app, name="memory")
        app.add_typer(file_app, name="file")
        app.add_typer(workflow_app, name="workflow")
        app.add_typer(cmd_app, name="cmd")
        app.add_typer(config_app, name="config")
        app.add_typer(dashboard_app, name="dashboard")
        
    • extended
      • cmd_tools.py 3.7 KB
        """Command execution tools with structured output."""
        from typing import Optional, Type, Dict, Any
        import subprocess
        import shlex
        import json
        from pathlib import Path
        from pydantic import BaseModel, Field
        
        try:
            from serena.tools.base import BaseTool
        except ImportError:
            class BaseTool:
                pass
        
        
        class RunCommandInput(BaseModel):
            command: str = Field(..., description="The shell command to execute")
            cwd: Optional[str] = Field(None, description="Working directory for execution")
            timeout: Optional[int] = Field(300, description="Timeout in seconds")
        
        
        class RunCommandTool(BaseTool):
            name = "run_command"
            description = "Execute a shell command on the system. Use with caution."
            args_schema: Type[BaseModel] = RunCommandInput
        
            def _execute(self, command: str, cwd: Optional[str], timeout: int) -> Dict[str, Any]:
                """Internal method returning structured data."""
                try:
                    process = subprocess.run(
                        command,
                        shell=True,
                        cwd=cwd,
                        capture_output=True,
                        text=True,
                        timeout=timeout
                    )
                    return {
                        "exit_code": process.returncode,
                        "stdout": process.stdout,
                        "stderr": process.stderr,
                    }
                except subprocess.TimeoutExpired:
                    return {
                        "error": {
                            "code": "TIMEOUT",
                            "message": f"Command timed out after {timeout} seconds"
                        }
                    }
                except Exception as e:
                    return {
                        "error": {
                            "code": "RUNTIME_ERROR",
                            "message": str(e)
                        }
                    }
        
            def _run(self, command: str, cwd: Optional[str] = None, timeout: int = 300) -> str:
                """BaseTool-compatible method returning JSON string."""
                result = self._execute(command, cwd, timeout)
                return json.dumps(result, ensure_ascii=False)
        
        
        class RunScriptInput(BaseModel):
            script_path: str = Field(..., description="Path to the script file")
            args: Optional[str] = Field(None, description="Arguments for the script")
        
        
        class RunScriptTool(BaseTool):
            name = "run_script"
            description = "Execute a local script file (python, bash, etc.)"
            args_schema: Type[BaseModel] = RunScriptInput
        
            def _execute(self, script_path: str, args: Optional[str]) -> Dict[str, Any]:
                """Internal method returning structured data."""
                import sys
        
                path = Path(script_path)
                if not path.exists():
                    return {
                        "error": {
                            "code": "FILE_NOT_FOUND",
                            "message": f"Script not found at {script_path}"
                        }
                    }
        
                cmd = []
                if path.suffix == '.py':
                    cmd = [sys.executable, str(path)]
                elif path.suffix == '.sh':
                    cmd = ['bash', str(path)]
                else:
                    cmd = [str(path)]
        
                if args:
                    cmd.extend(shlex.split(args))
        
                try:
                    process = subprocess.run(
                        cmd,
                        capture_output=True,
                        text=True
                    )
                    return {
                        "exit_code": process.returncode,
                        "stdout": process.stdout,
                        "stderr": process.stderr,
                    }
                except Exception as e:
                    return {
                        "error": {
                            "code": "RUNTIME_ERROR",
                            "message": str(e)
                        }
                    }
        
            def _run(self, script_path: str, args: Optional[str] = None) -> str:
                """BaseTool-compatible method returning JSON string."""
                result = self._execute(script_path, args)
                return json.dumps(result, ensure_ascii=False)
        
      • config_tools.py 4.9 KB
        """Configuration file tools with structured output."""
        from typing import Optional, Type, Dict, Any
        import json
        import yaml
        from pathlib import Path
        from pydantic import BaseModel, Field
        
        try:
            from serena.tools.base import BaseTool
        except ImportError:
            class BaseTool:
                pass
        
        
        class ReadConfigInput(BaseModel):
            path: str = Field(..., description="Path to configuration file")
            format: Optional[str] = Field(None, description="Format (json, yaml). Auto-detected if None")
        
        
        class ReadConfigTool(BaseTool):
            name = "read_config"
            description = "Read a configuration file (JSON/YAML) into a dictionary"
            args_schema: Type[BaseModel] = ReadConfigInput
        
            def _execute(self, path: str, format: Optional[str]) -> Dict[str, Any]:
                """Internal method returning structured data."""
                file_path = Path(path)
                if not file_path.exists():
                    return {
                        "error": {
                            "code": "FILE_NOT_FOUND",
                            "message": f"File not found at {path}"
                        }
                    }
        
                try:
                    content = file_path.read_text(encoding='utf-8')
                    fmt = format.lower() if format else file_path.suffix.lstrip('.')
        
                    if fmt in ['json']:
                        data = json.loads(content)
                    elif fmt in ['yaml', 'yml']:
                        data = yaml.safe_load(content)
                    else:
                        return {
                            "error": {
                                "code": "UNSUPPORTED_FORMAT",
                                "message": f"Unsupported format {fmt}. Use json or yaml."
                            }
                        }
        
                    return {"config": data}
                except Exception as e:
                    return {
                        "error": {
                            "code": "PARSE_ERROR",
                            "message": str(e)
                        }
                    }
        
            def _run(self, path: str, format: Optional[str] = None) -> str:
                """BaseTool-compatible method returning JSON string."""
                result = self._execute(path, format)
                return json.dumps(result, indent=2, ensure_ascii=False)
        
        
        class UpdateConfigInput(BaseModel):
            path: str = Field(..., description="Path to configuration file")
            key: str = Field(..., description="Dot-notation key to update (e.g. 'server.port')")
            value: str = Field(..., description="New value (parsed as JSON if possible)")
        
        
        class UpdateConfigTool(BaseTool):
            name = "update_config"
            description = "Update a specific key in a configuration file"
            args_schema: Type[BaseModel] = UpdateConfigInput
        
            def _set_nested(self, data: Dict, key: str, value: Any):
                """Set nested dictionary value using dot notation."""
                parts = key.split('.')
                current = data
                for part in parts[:-1]:
                    if part not in current:
                        current[part] = {}
                    current = current[part]
                    if not isinstance(current, dict):
                        raise ValueError(f"Cannot traverse path {key}: {part} is not a dict")
                current[parts[-1]] = value
        
            def _parse_value(self, value_str: str) -> Any:
                """Parse value string as JSON or return as string."""
                try:
                    return json.loads(value_str)
                except:
                    return value_str
        
            def _execute(self, path: str, key: str, value: str) -> Dict[str, Any]:
                """Internal method returning structured data."""
                file_path = Path(path)
                if not file_path.exists():
                    return {
                        "error": {
                            "code": "FILE_NOT_FOUND",
                            "message": f"File not found at {path}"
                        }
                    }
        
                try:
                    content = file_path.read_text(encoding='utf-8')
                    fmt = file_path.suffix.lstrip('.').lower()
        
                    if fmt == 'json':
                        data = json.loads(content)
                    elif fmt in ['yaml', 'yml']:
                        data = yaml.safe_load(content) or {}
                    else:
                        return {
                            "error": {
                                "code": "UNSUPPORTED_FORMAT",
                                "message": f"Unsupported format {fmt}"
                            }
                        }
        
                    parsed_value = self._parse_value(value)
                    self._set_nested(data, key, parsed_value)
        
                    if fmt == 'json':
                        new_content = json.dumps(data, indent=2, ensure_ascii=False)
                    else:
                        new_content = yaml.safe_dump(data, allow_unicode=True)
        
                    file_path.write_text(new_content, encoding='utf-8')
                    return {"message": "Config updated successfully", "key": key, "value": parsed_value}
        
                except Exception as e:
                    return {
                        "error": {
                            "code": "UPDATE_ERROR",
                            "message": str(e)
                        }
                    }
        
            def _run(self, path: str, key: str, value: str) -> str:
                """BaseTool-compatible method returning JSON string."""
                result = self._execute(path, key, value)
                return json.dumps(result, ensure_ascii=False)
        
      • __init__.py 30 B
        """Extended tools package."""
        
    • server
      • dashboard_server.py 14.1 KB
        """Lightweight Flask dashboard server for Serena CLI wrapper."""
        
        import os
        import socket
        import threading
        from pathlib import Path
        from typing import Any, Optional
        
        from flask import Flask, Response, redirect, request, send_from_directory
        
        from ..core import SerenaCore
        
        
        class DashboardServer:
            """Flask-based dashboard HTTP server."""
        
            DEFAULT_START_PORT = 0x5EDA
            UPSTREAM_DASHBOARD_DIR = Path(
                os.environ.get(
                    "SERENA_UPSTREAM_DASHBOARD_DIR",
                    "E:/Python/PythonProject1/serena/src/serena/resources/dashboard",
                )
            )
        
            def __init__(self, core: SerenaCore, dashboard_dir: Optional[Path | str] = None):
                self._core = core
                self._dashboard_dir = self._resolve_dashboard_dir(dashboard_dir)
                self._app = Flask(__name__)
                self._setup_routes()
        
                # Register current project in global Serena config
                self._update_global_serena_config()
        
            @property
            def app(self) -> Flask:
                return self._app
        
            @staticmethod
            def _normalize_names(value: Any) -> list[str]:
                if value is None:
                    return []
                if isinstance(value, list):
                    normalized = [str(item) for item in value if item is not None and str(item)]
                    return sorted(set(normalized))
                value_str = str(value)
                return [value_str] if value_str else []
        
            @staticmethod
            def _build_active_project_name(project_path: Optional[str]) -> Optional[str]:
                if not project_path:
                    return None
                path = Path(project_path)
                return path.name or project_path
        
            def _resolve_dashboard_dir(self, dashboard_dir: Optional[Path | str]) -> Path:
                candidates: list[Path] = []
                if dashboard_dir is not None:
                    candidates.append(Path(dashboard_dir))
                candidates.append(Path(__file__).resolve().parents[1] / "resources" / "dashboard")
                candidates.append(self.UPSTREAM_DASHBOARD_DIR)
        
                for candidate in candidates:
                    if (candidate / "index.html").exists():
                        return candidate
        
                searched = ", ".join(str(path) for path in candidates)
                raise FileNotFoundError(f"Serena dashboard static files not found. searched=[{searched}]")
        
            def _update_global_serena_config(self) -> None:
                """Update global Serena config to register the current project."""
                try:
                    import yaml
                    from pathlib import Path
        
                    # Get current project path
                    dashboard_info = self._core.get_dashboard_info()
                    project_path = dashboard_info.get("active_project_path")
                    if not project_path:
                        return
        
                    project_path = str(Path(project_path).resolve())
        
                    # Load global config
                    global_config_path = Path.home() / ".serena" / "serena_config.yml"
                    if not global_config_path.exists():
                        return
        
                    with open(global_config_path, 'r', encoding='utf-8') as f:
                        config = yaml.safe_load(f) or {}
        
                    # Add project to projects list if not already present
                    projects = config.get("projects", [])
                    if project_path not in projects:
                        projects.append(project_path)
                        config["projects"] = projects
        
                        # Save updated config
                        with open(global_config_path, 'w', encoding='utf-8') as f:
                            yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
        
                except Exception as e:
                    # Don't fail if global config update fails
                    import sys
                    print(f"[WARNING] Failed to update global Serena config: {e}", file=sys.stderr)
        
            def _build_config_overview(self) -> dict[str, Any]:
                dashboard_info = self._core.get_dashboard_info()
        
                project_path_raw = dashboard_info.get("active_project_path")
                project_path = str(project_path_raw) if project_path_raw else None
                project_name = self._build_active_project_name(project_path)
        
                context_name = str(dashboard_info.get("context") or "agent")
        
                active_tools = self._normalize_names(dashboard_info.get("active_tools"))
                available_tools = self._normalize_names(dashboard_info.get("available_tools"))
                if not available_tools:
                    available_tools = list(active_tools)
        
                active_modes = self._normalize_names(dashboard_info.get("active_modes"))
                available_modes = self._normalize_names(dashboard_info.get("available_modes"))
                available_modes = sorted(set(available_modes + active_modes))
        
                available_contexts = self._normalize_names(dashboard_info.get("available_contexts"))
                available_contexts = sorted(set(available_contexts + [context_name]))
        
                active_tool_set = set(active_tools)
                active_mode_set = set(active_modes)
        
                registered_projects: list[dict[str, str | bool]] = []
                if project_path:
                    registered_projects.append(
                        {
                            "name": project_name or project_path,
                            "path": project_path,
                            "is_active": True,
                        }
                    )
        
                return {
                    "active_project": {
                        "name": project_name,
                        "language": None,
                        "path": project_path,
                    },
                    "context": {
                        "name": context_name,
                        "description": "",
                        "path": "",
                    },
                    "modes": [
                        {
                            "name": mode_name,
                            "description": "",
                            "path": "",
                        }
                        for mode_name in active_modes
                    ],
                    "active_tools": active_tools,
                    "tool_stats_summary": {
                        tool_name: {"num_calls": 0}
                        for tool_name in active_tools
                    },
                    "registered_projects": registered_projects,
                    "available_tools": [
                        {"name": tool_name, "is_active": False}
                        for tool_name in available_tools
                        if tool_name not in active_tool_set
                    ],
                    "available_modes": [
                        {
                            "name": mode_name,
                            "is_active": mode_name in active_mode_set,
                            "path": "",
                        }
                        for mode_name in available_modes
                    ],
                    "available_contexts": [
                        {
                            "name": context_item,
                            "is_active": context_item == context_name,
                            "path": "",
                        }
                        for context_item in available_contexts
                    ],
                    "available_memories": None,
                    "jetbrains_mode": False,
                    "languages": [],
                    "encoding": None,
                    "current_client": None,
                }
        
            def _setup_routes(self) -> None:
                @self._app.route("/")
                def root() -> Response:
                    return redirect("/dashboard/index.html", code=302)
        
                @self._app.route("/dashboard")
                def dashboard_root() -> Response:
                    return redirect("/dashboard/index.html", code=302)
        
                @self._app.route("/dashboard/")
                def dashboard_index_redirect() -> Response:
                    return redirect("/dashboard/index.html", code=302)
        
                @self._app.route("/dashboard/index.html")
                def dashboard_index() -> Response:
                    return send_from_directory(str(self._dashboard_dir), "index.html")
        
                @self._app.route("/dashboard/<path:filename>")
                def dashboard_files(filename: str) -> Response:
                    return send_from_directory(str(self._dashboard_dir), filename)
        
                @self._app.route("/heartbeat", methods=["GET"])
                def heartbeat() -> dict[str, str]:
                    return {"status": "alive"}
        
                @self._app.route("/get_config_overview", methods=["GET"])
                def get_config_overview() -> dict[str, Any]:
                    return self._build_config_overview()
        
                @self._app.route("/get_tool_names", methods=["GET"])
                def get_tool_names() -> dict[str, list[str]]:
                    return {"tool_names": self._normalize_names(self._core.list_tools(scope="active"))}
        
                @self._app.route("/get_log_messages", methods=["POST"])
                def get_log_messages() -> dict[str, Any]:
                    request_data = request.get_json(silent=True) or {}
                    if not isinstance(request_data, dict):
                        request_data = {}
                    try:
                        start_idx = int(request_data.get("start_idx", 0))
                    except (TypeError, ValueError):
                        start_idx = 0
        
                    dashboard_info = self._core.get_dashboard_info()
                    project_path = dashboard_info.get("active_project_path")
                    active_project = self._build_active_project_name(str(project_path)) if project_path else None
                    return {
                        "messages": [],
                        "max_idx": start_idx - 1,
                        "active_project": active_project,
                    }
        
                @self._app.route("/queued_task_executions", methods=["GET"])
                def queued_task_executions() -> dict[str, list]:
                    """Return empty executions queue (CLI wrapper doesn't track executions)."""
                    return {"queued_executions": []}
        
                @self._app.route("/last_execution", methods=["GET"])
                def last_execution() -> dict[str, Any]:
                    """Return empty last execution (CLI wrapper doesn't track executions)."""
                    return {
                        "task_id": None,
                        "is_running": False,
                        "name": None,
                        "finished_successfully": False,
                        "logged": False,
                    }
        
                @self._app.route("/get_serena_config", methods=["GET"])
                def get_serena_config() -> dict[str, Any]:
                    """Return Serena configuration from .env file."""
                    from pathlib import Path
                    import os
        
                    # Try to find .env file
                    env_paths = [
                        Path(__file__).resolve().parents[2] / ".env",  # skills/serena/.env
                        Path.cwd() / ".env",
                    ]
        
                    config_content = ""
                    config_path = None
        
                    for env_path in env_paths:
                        if env_path.exists():
                            config_path = str(env_path)
                            try:
                                with open(env_path, 'r', encoding='utf-8') as f:
                                    config_content = f.read()
                                break
                            except Exception:
                                pass
        
                    if not config_content:
                        # Return default template if no .env found
                        config_content = """# Serena CLI Configuration
        SERENA_CONTEXT=claude-code
        SERENA_MODES=interactive,editing,onboarding
        SERENA_PROJECT=.
        SERENA_DASHBOARD_ENABLED=true
        SERENA_DASHBOARD_PORT=24282
        """
                        config_path = str(env_paths[0])
        
                    return {
                        "status": "success",
                        "content": config_content,
                        "config": config_content,  # Backward compatibility
                        "path": config_path,
                    }
        
                @self._app.route("/save_serena_config", methods=["POST"])
                @self._app.route("/update_serena_config", methods=["POST"])
                def update_serena_config() -> dict[str, Any]:
                    """Update Serena configuration in .env file."""
                    from pathlib import Path
        
                    request_data = request.get_json(silent=True) or {}
                    # Support both 'content' (frontend) and 'config' (backward compatibility)
                    new_config = request_data.get("content")
                    if new_config is None:
                        new_config = request_data.get("config", "")
        
                    if not new_config:
                        return {
                            "status": "error",
                            "message": "No configuration provided",
                            "success": False,
                        }
        
                    # Try to find .env file
                    env_paths = [
                        Path(__file__).resolve().parents[2] / ".env",
                        Path.cwd() / ".env",
                    ]
        
                    config_path = None
                    for env_path in env_paths:
                        if env_path.exists():
                            config_path = env_path
                            break
        
                    if not config_path:
                        # Create new .env file
                        config_path = env_paths[0]
        
                    try:
                        config_path.parent.mkdir(parents=True, exist_ok=True)
                        with open(config_path, 'w', encoding='utf-8') as f:
                            f.write(new_config)
        
                        return {
                            "status": "success",
                            "message": "Serena config saved successfully",
                            "path": str(config_path),
                            "success": True,
                        }
                    except Exception as e:
                        return {
                            "status": "error",
                            "message": str(e),
                            "success": False,
                        }
        
            @staticmethod
            def _find_first_free_port(start_port: int, host: str) -> int:
                port = max(0, start_port)
                while port <= 65535:
                    try:
                        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
                            sock.bind((host, port))
                            return port
                    except OSError:
                        port += 1
                raise RuntimeError(f"No free ports found starting from {start_port}")
        
            @staticmethod
            def get_dashboard_url(host: str, port: int) -> str:
                browser_host = "localhost" if host in {"0.0.0.0", "::"} else host
                # Use canonical URL to avoid equivalent-path duplication in browser tabs
                return f"http://{browser_host}:{port}/dashboard/index.html"
        
            def run(self, host: str, port: int) -> int:
                from flask import cli
                cli.show_server_banner = lambda *args, **kwargs: None
                self._app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True)
                return port
        
            def run_in_thread(self, host: str = "127.0.0.1", port: Optional[int] = None) -> tuple[threading.Thread, int]:
                resolved_port = port if port is not None and port > 0 else self._find_first_free_port(self.DEFAULT_START_PORT, host)
                thread = threading.Thread(target=lambda: self.run(host=host, port=resolved_port), daemon=True)
                thread.start()
                return thread, resolved_port
        
      • __init__.py 110 B
        """Dashboard server package."""
        
        from .dashboard_server import DashboardServer
        
        __all__ = ["DashboardServer"]
        
    • tests
      • conftest.py 256 B
        """Pytest configuration for Serena tools tests."""
        import sys
        from pathlib import Path
        
        # Add skills/serena directory to Python path so tools can be imported as a package
        serena_dir = Path(__file__).parent.parent.parent
        sys.path.insert(0, str(serena_dir))
        
      • test_cli_dashboard.py 3 KB
        """Tests for dashboard CLI commands."""
        
        
        def test_dashboard_info_outputs_result_wrapper():
            """Test that dashboard info wraps output under result key."""
            from unittest.mock import Mock, patch
        
            with patch('tools.cli.dashboard.output_json') as mock_output:
                from tools.cli import State
                from tools.cli.dashboard import info
        
                mock_core = Mock()
                State.core = mock_core
                mock_core.get_dashboard_info.return_value = {"context": "agent"}
        
                info()
        
                mock_core.get_dashboard_info.assert_called_once_with()
                mock_output.assert_called_once_with({"result": {"context": "agent"}})
        
        
        def test_dashboard_tools_outputs_active_and_available():
            """Test dashboard tools command output structure."""
            from unittest.mock import Mock, patch, call
        
            with patch('tools.cli.dashboard.output_json') as mock_output:
                from tools.cli import State
                from tools.cli.dashboard import tools
        
                mock_core = Mock()
                State.core = mock_core
                mock_core.list_tools.side_effect = [["read_memory"], ["read_memory", "find_symbol"]]
        
                tools()
        
                mock_core.list_tools.assert_has_calls([call(scope="active"), call(scope="all")])
                mock_output.assert_called_once_with(
                    {
                        "result": {
                            "active_count": 1,
                            "active": ["read_memory"],
                            "available_count": 2,
                            "available": ["read_memory", "find_symbol"],
                        }
                    }
                )
        
        
        def test_dashboard_modes_outputs_active_and_available():
            """Test dashboard modes command output structure."""
            from unittest.mock import Mock, patch, call
        
            with patch('tools.cli.dashboard.output_json') as mock_output:
                from tools.cli import State
                from tools.cli.dashboard import modes
        
                mock_core = Mock()
                State.core = mock_core
                mock_core.list_modes.side_effect = [["interactive"], ["interactive", "editing"]]
        
                modes()
        
                mock_core.list_modes.assert_has_calls([call(scope="active"), call(scope="all")])
                mock_output.assert_called_once_with(
                    {"result": {"active": ["interactive"], "available": ["interactive", "editing"]}}
                )
        
        
        def test_dashboard_contexts_outputs_active_and_available():
            """Test dashboard contexts command output structure."""
            from unittest.mock import Mock, patch, call
        
            with patch('tools.cli.dashboard.output_json') as mock_output:
                from tools.cli import State
                from tools.cli.dashboard import contexts
        
                mock_core = Mock()
                State.core = mock_core
                mock_core.list_contexts.side_effect = [["codex"], ["agent", "claude-code", "codex", "ide"]]
        
                contexts()
        
                mock_core.list_contexts.assert_has_calls([call(scope="active"), call(scope="all")])
                mock_output.assert_called_once_with(
                    {
                        "result": {
                            "active": ["codex"],
                            "available": ["agent", "claude-code", "codex", "ide"],
                        }
                    }
                )
        
      • test_cli_file.py 3.4 KB
        """Tests for file CLI commands parameter mapping."""
        
        
        def test_search_pattern_parameter_mapping():
            """Test that search_pattern function calls tool with correct parameters."""
            from unittest.mock import Mock, patch
        
            # Mock output_json to avoid side effects
            with patch('tools.output.output_json'):
                from tools.cli import State
                from tools.cli.file import search_pattern
        
                # Setup mock
                mock_core = Mock()
                State.core = mock_core
                mock_core.call_tool.return_value = {"results": []}
        
                # Execute
                search_pattern("test_pattern", path=None)
        
                # Verify - should use substring_pattern, not pattern
                mock_core.call_tool.assert_called_once_with(
                    "search_for_pattern",
                    substring_pattern="test_pattern",
                    relative_path=None,
                )
        
        
        def test_search_pattern_with_path():
            """Test search with path restriction."""
            from unittest.mock import Mock, patch
        
            with patch('tools.output.output_json'):
                from tools.cli import State
                from tools.cli.file import search_pattern
        
                # Setup mock
                mock_core = Mock()
                State.core = mock_core
                mock_core.call_tool.return_value = {"results": []}
        
                # Execute
                search_pattern("test_pattern", path="src")
        
                # Verify
                mock_core.call_tool.assert_called_once_with(
                    "search_for_pattern",
                    substring_pattern="test_pattern",
                    relative_path="src",
                )
        
        
        def test_find_file_parameter_mapping():
            """Test that find_file function calls tool with correct parameters."""
            from unittest.mock import Mock, patch
        
            with patch('tools.output.output_json'):
                from tools.cli import State
                from tools.cli.file import find_file
        
                # Setup mock
                mock_core = Mock()
                State.core = mock_core
                mock_core.call_tool.return_value = {"files": []}
        
                # Execute
                find_file("*.py")
        
                # Verify - should use file_mask and relative_path="."
                mock_core.call_tool.assert_called_once_with(
                    "find_file",
                    file_mask="*.py",
                    relative_path=".",
                )
        
        
        def test_list_dir_default_path():
            """Test that list_dir defaults to relative_path='.' when path is None."""
            from unittest.mock import Mock, patch
        
            with patch('tools.output.output_json'):
                from tools.cli import State
                from tools.cli.file import list_dir
        
                # Setup mock
                mock_core = Mock()
                State.core = mock_core
                mock_core.call_tool.return_value = {"entries": []}
        
                # Execute without path
                list_dir(path=None, recursive=False)
        
                # Verify - should default to relative_path="."
                mock_core.call_tool.assert_called_once_with(
                    "list_dir",
                    relative_path=".",
                    recursive=False,
                )
        
        
        def test_list_dir_with_path():
            """Test list_dir with explicit path."""
            from unittest.mock import Mock, patch
        
            with patch('tools.output.output_json'):
                from tools.cli import State
                from tools.cli.file import list_dir
        
                # Setup mock
                mock_core = Mock()
                State.core = mock_core
                mock_core.call_tool.return_value = {"entries": []}
        
                # Execute with path
                list_dir(path="src", recursive=True)
        
                # Verify
                mock_core.call_tool.assert_called_once_with(
                    "list_dir",
                    relative_path="src",
                    recursive=True,
                )
        
      • test_cli_memory.py 2.6 KB
        """Tests for memory CLI commands parameter mapping."""
        
        
        def test_read_memory_parameter_mapping():
            """Test that read_memory maps name to memory_file_name."""
            from unittest.mock import Mock, patch
        
            with patch('tools.output.output_json'):
                from tools.cli import State
                from tools.cli.memory import read_memory
        
                # Setup mock
                mock_core = Mock()
                State.core = mock_core
                mock_core.call_tool.return_value = {"content": "test"}
        
                # Execute
                read_memory("project_notes")
        
                # Verify
                mock_core.call_tool.assert_called_once_with(
                    "read_memory",
                    memory_file_name="project_notes",
                )
        
        
        def test_write_memory_parameter_mapping():
            """Test that write_memory maps name to memory_name."""
            from unittest.mock import Mock, patch
        
            with patch('tools.output.output_json'):
                from tools.cli import State
                from tools.cli.memory import write_memory
        
                # Setup mock
                mock_core = Mock()
                State.core = mock_core
                mock_core.call_tool.return_value = {"success": True}
        
                # Execute
                write_memory("api_notes", content="test content")
        
                # Verify
                mock_core.call_tool.assert_called_once_with(
                    "write_memory",
                    memory_name="api_notes",
                    content="test content",
                )
        
        
        def test_edit_memory_parameter_mapping():
            """Test that edit_memory maps to write_memory with memory_name."""
            from unittest.mock import Mock, patch
        
            with patch('tools.output.output_json'):
                from tools.cli import State
                from tools.cli.memory import edit_memory
        
                # Setup mock
                mock_core = Mock()
                State.core = mock_core
                mock_core.call_tool.return_value = {"success": True}
        
                # Execute
                edit_memory("notes", content="new content")
        
                # Verify - edit should use write_memory tool
                mock_core.call_tool.assert_called_once_with(
                    "write_memory",
                    memory_name="notes",
                    content="new content",
                )
        
        
        def test_delete_memory_parameter_mapping():
            """Test that delete_memory maps name to memory_file_name."""
            from unittest.mock import Mock, patch
        
            with patch('tools.output.output_json'):
                from tools.cli import State
                from tools.cli.memory import delete_memory
        
                # Setup mock
                mock_core = Mock()
                State.core = mock_core
                mock_core.call_tool.return_value = {"success": True}
        
                # Execute
                delete_memory("old_notes")
        
                # Verify
                mock_core.call_tool.assert_called_once_with(
                    "delete_memory",
                    memory_file_name="old_notes",
                )
        
      • test_core.py 5.4 KB
        """Tests for SerenaCore context detection."""
        import os
        from pathlib import Path
        from unittest.mock import patch
        import tempfile
        import shutil
        
        
        def test_context_detection_claude_code_directory():
            """Test Claude Code detection via .claude directory."""
            from tools.core import SerenaCore
        
            with tempfile.TemporaryDirectory() as tmpdir:
                tmppath = Path(tmpdir)
                (tmppath / ".claude").mkdir()
        
                core = SerenaCore(project=str(tmppath))
                assert core.context == "claude-code"
        
        
        def test_context_detection_claude_code_env():
            """Test Claude Code detection via CLAUDECODE environment variable."""
            from tools.core import SerenaCore
        
            with tempfile.TemporaryDirectory() as tmpdir:
                with patch.dict(os.environ, {"CLAUDECODE": "1"}):
                    core = SerenaCore(project=tmpdir)
                    assert core.context == "claude-code"
        
        
        def test_context_detection_vscode_directory():
            """Test VSCode detection via .vscode directory."""
            from tools.core import SerenaCore
        
            with tempfile.TemporaryDirectory() as tmpdir:
                tmppath = Path(tmpdir)
                (tmppath / ".vscode").mkdir()
        
                core = SerenaCore(project=str(tmppath))
                assert core.context == "ide"
        
        
        def test_context_detection_vscode_env():
            """Test VSCode detection via VSCODE_PID environment variable."""
            from tools.core import SerenaCore
        
            with tempfile.TemporaryDirectory() as tmpdir:
                with patch.dict(os.environ, {"VSCODE_PID": "12345"}):
                    core = SerenaCore(project=tmpdir)
                    assert core.context == "ide"
        
        
        def test_context_detection_jetbrains_directory():
            """Test JetBrains detection via .idea directory."""
            from tools.core import SerenaCore
        
            with tempfile.TemporaryDirectory() as tmpdir:
                tmppath = Path(tmpdir)
                (tmppath / ".idea").mkdir()
        
                core = SerenaCore(project=str(tmppath))
                assert core.context == "ide"
        
        
        def test_context_detection_jetbrains_env():
            """Test JetBrains detection via IDEA_INITIAL_DIRECTORY environment variable."""
            from tools.core import SerenaCore
        
            with tempfile.TemporaryDirectory() as tmpdir:
                with patch.dict(os.environ, {"IDEA_INITIAL_DIRECTORY": tmpdir}):
                    core = SerenaCore(project=tmpdir)
                    assert core.context == "ide"
        
        
        def test_context_detection_codex_directory():
            """Test Codex detection via .codex directory."""
            from tools.core import SerenaCore
        
            with tempfile.TemporaryDirectory() as tmpdir:
                tmppath = Path(tmpdir)
                (tmppath / ".codex").mkdir()
        
                core = SerenaCore(project=str(tmppath))
                assert core.context == "codex"
        
        
        def test_context_detection_codex_env():
            """Test Codex detection via CODEX_CLI_SESSION environment variable."""
            from tools.core import SerenaCore
        
            with tempfile.TemporaryDirectory() as tmpdir:
                with patch.dict(os.environ, {"CODEX_CLI_SESSION": "test-session"}):
                    core = SerenaCore(project=tmpdir)
                    assert core.context == "codex"
        
        
        def test_context_detection_default():
            """Test default context when no markers exist."""
            from tools.core import SerenaCore
        
            with tempfile.TemporaryDirectory() as tmpdir:
                # Clean environment - remove keys if they exist
                env_backup = {}
                keys_to_remove = ["CLAUDE_CODE_SESSION", "VSCODE_PID", "IDEA_INITIAL_DIRECTORY", "CODEX_CLI_SESSION"]
                for key in keys_to_remove:
                    if key in os.environ:
                        env_backup[key] = os.environ.pop(key)
        
                try:
                    core = SerenaCore(project=tmpdir)
                    assert core.context == "agent"
                finally:
                    # Restore environment
                    os.environ.update(env_backup)
        
        
        def test_context_detection_priority_directory_over_env():
            """Test that directory markers take precedence over environment variables."""
            from tools.core import SerenaCore
        
            with tempfile.TemporaryDirectory() as tmpdir:
                tmppath = Path(tmpdir)
                (tmppath / ".claude").mkdir()
        
                # Set conflicting environment variable
                with patch.dict(os.environ, {"CODEX_CLI_SESSION": "test-session"}):
                    core = SerenaCore(project=str(tmppath))
                    # Should detect claude-code from directory, not codex from env
                    assert core.context == "claude-code"
        
        
        def test_serena_core_initializes_config_path():
            """Test that SerenaCore initializes config path on creation."""
            from tools.core import SerenaCore
        
            with tempfile.TemporaryDirectory() as tmpdir:
                with patch.dict(os.environ, {"SERENA_HOME": tmpdir}):
                    core = SerenaCore(project=tmpdir)
        
                    # Config file should be created
                    config_file = Path(tmpdir) / "serena_config.yml"
                    assert config_file.exists()
        
        
        def test_get_dashboard_info_without_agent_uses_wrapper_state():
            """Test dashboard info fallback when agent is unavailable."""
            from tools.core import SerenaCore
        
            with tempfile.TemporaryDirectory() as tmpdir:
                core = SerenaCore(project=tmpdir, context="codex", modes=["interactive", "editing"])
        
                with patch.object(core, "_ensure_agent", side_effect=RuntimeError("agent unavailable")):
                    info = core.get_dashboard_info()
        
                assert info["active_project_path"] == str(Path(tmpdir).resolve())
                assert info["context"] == "codex"
                assert info["active_modes"] == ["editing", "interactive"]
                assert info["active_tools_count"] == 0
                assert "codex" in info["available_contexts"]
                assert "interactive" in info["available_modes"]
        
      • test_module_path.py 3 KB
        """Regression test for module path fix.
        
        This test ensures that the serena tools can be executed from the serena root directory
        using 'python -m tools' command, which is the correct usage after skill installation.
        
        Issue: Previously, SKILL.md and README.md used 'python -m skills.serena.tools' which
        assumed execution from the parent directory of skills/, causing ModuleNotFoundError
        when executed from the serena root directory.
        
        Fix: Changed all command examples to 'python -m tools' for execution from serena root.
        """
        import subprocess
        import sys
        from pathlib import Path
        
        
        def test_module_execution_from_serena_root():
            """Test that 'python -m tools' works from serena root directory."""
            serena_root = Path(__file__).parent.parent.parent
        
            # Test basic command execution
            result = subprocess.run(
                [sys.executable, "-m", "tools", "--help"],
                cwd=serena_root,
                capture_output=True,
                text=True,
                timeout=10
            )
        
            assert result.returncode == 0, f"Command failed with: {result.stderr}"
            assert "serena" in result.stdout.lower(), "Help text should mention serena"
        
        
        def test_symbol_command_execution():
            """Test that symbol commands work correctly."""
            serena_root = Path(__file__).parent.parent.parent
        
            result = subprocess.run(
                [sys.executable, "-m", "tools", "symbol", "--help"],
                cwd=serena_root,
                capture_output=True,
                text=True,
                timeout=10
            )
        
            assert result.returncode == 0, f"Symbol command failed: {result.stderr}"
            assert "symbol" in result.stdout.lower()
        
        
        def test_dashboard_command_execution():
            """Test that dashboard commands work correctly."""
            serena_root = Path(__file__).parent.parent.parent
        
            result = subprocess.run(
                [sys.executable, "-m", "tools", "dashboard", "info"],
                cwd=serena_root,
                capture_output=True,
                text=True,
                timeout=10
            )
        
            assert result.returncode == 0, f"Dashboard command failed: {result.stderr}"
            # Should return JSON output
            assert "{" in result.stdout
        
        
        def test_documentation_uses_correct_module_path():
            """Test that documentation files use 'python -m tools' instead of 'python -m skills.serena.tools'."""
            serena_root = Path(__file__).parent.parent.parent
        
            # Check SKILL.md
            skill_md = (serena_root / "SKILL.md").read_text(encoding="utf-8")
            assert "skills.serena.tools" not in skill_md, "SKILL.md should not contain 'skills.serena.tools'"
            assert "python -m tools" in skill_md, "SKILL.md should contain 'python -m tools'"
        
            # Check README.md
            readme_md = (serena_root / "README.md").read_text(encoding="utf-8")
            assert "skills.serena.tools" not in readme_md, "README.md should not contain 'skills.serena.tools'"
            assert "python -m tools" in readme_md, "README.md should contain 'python -m tools'"
        
        
        if __name__ == "__main__":
            test_module_execution_from_serena_root()
            test_symbol_command_execution()
            test_dashboard_command_execution()
            test_documentation_uses_correct_module_path()
            print("✅ All regression tests passed!")
        
      • test_paths.py 2.8 KB
        """Tests for path resolution module."""
        import os
        from pathlib import Path
        from unittest.mock import patch
        import tempfile
        
        
        def test_get_config_dir_default():
            """Test default config directory resolution."""
            from tools.paths import SerenaToolsPaths
        
            paths = SerenaToolsPaths()
            config_dir = paths.get_config_dir()
        
            # Should resolve to ~/.serena
            expected = Path.home() / ".serena"
            assert config_dir == expected
        
        
        def test_get_config_dir_with_serena_home():
            """Test config directory with SERENA_HOME override."""
            from tools.paths import SerenaToolsPaths
        
            with tempfile.TemporaryDirectory() as tmpdir:
                with patch.dict(os.environ, {"SERENA_HOME": tmpdir}):
                    paths = SerenaToolsPaths()
                    config_dir = paths.get_config_dir()
        
                    assert config_dir == Path(tmpdir)
        
        
        def test_get_config_file_path():
            """Test config file path resolution."""
            from tools.paths import SerenaToolsPaths
        
            paths = SerenaToolsPaths()
            config_file = paths.get_config_file_path()
        
            # Should resolve to ~/.serena/serena_config.yml
            expected = Path.home() / ".serena" / "serena_config.yml"
            assert config_file == expected
        
        
        def test_ensure_config_exists_creates_directory():
            """Test that ensure_config_exists creates config directory."""
            from tools.paths import SerenaToolsPaths
        
            with tempfile.TemporaryDirectory() as tmpdir:
                with patch.dict(os.environ, {"SERENA_HOME": tmpdir}):
                    paths = SerenaToolsPaths()
                    config_file = paths.ensure_config_exists()
        
                    # Directory should be created
                    assert config_file.parent.exists()
                    assert config_file.parent.is_dir()
        
        
        def test_ensure_config_exists_creates_file():
            """Test that ensure_config_exists creates config file if missing."""
            from tools.paths import SerenaToolsPaths
        
            with tempfile.TemporaryDirectory() as tmpdir:
                with patch.dict(os.environ, {"SERENA_HOME": tmpdir}):
                    paths = SerenaToolsPaths()
                    config_file = paths.ensure_config_exists()
        
                    # Config file should be created
                    assert config_file.exists()
                    assert config_file.is_file()
        
        
        def test_ensure_config_exists_preserves_existing():
            """Test that ensure_config_exists preserves existing config."""
            from tools.paths import SerenaToolsPaths
        
            with tempfile.TemporaryDirectory() as tmpdir:
                with patch.dict(os.environ, {"SERENA_HOME": tmpdir}):
                    # Create existing config
                    config_dir = Path(tmpdir)
                    config_dir.mkdir(exist_ok=True)
                    config_file = config_dir / "serena_config.yml"
                    config_file.write_text("existing: config")
        
                    paths = SerenaToolsPaths()
                    result = paths.ensure_config_exists()
        
                    # Should preserve existing content
                    assert result.read_text() == "existing: config"
        
      • __init__.py 30 B
        """Tests for Serena Tools."""
        
    • core.py 7.8 KB
      """Serena Core Wrapper."""
      
      import os
      from pathlib import Path
      from typing import Any, Optional, List, Dict
      
      try:
          from serena.agent import SerenaAgent, SerenaConfig
          from serena.tools import ToolRegistry
      except ImportError:
          raise ImportError("serena-agent is not installed")
      
      from .paths import SerenaToolsPaths
      
      
      class SerenaCore:
          """Wrapper for SerenaAgent providing CLI-friendly interface and context awareness."""
      
          KNOWN_CONTEXTS = ["agent", "claude-code", "ide", "codex"]
      
          def __init__(
              self,
              project: Optional[str] = None,
              context: Optional[str] = None,
              modes: Optional[List[str]] = None,
          ):
              self.project_path = Path(project).resolve() if project else Path.cwd()
              self.context = context or self._detect_context(self.project_path)
              self.modes = modes or ["interactive", "editing"]
              self._agent: Optional[SerenaAgent] = None
              self._extended_tools: Dict[str, Any] = {}
      
              # Initialize config path
              self._paths = SerenaToolsPaths()
              self._paths.ensure_config_exists()
      
          def _detect_context(self, path: Path) -> str:
              """Detect environment context from SERENA_CONTEXT env var only.
      
              No automatic detection - context must be explicitly configured.
              Default: desktop-app (full toolset)
              """
              # Read from environment variable only
              context = os.environ.get("SERENA_CONTEXT", "desktop-app")
              return context
      
          def _resolve_context(self) -> Optional[Any]:
              """Convert string context to SerenaAgentContext if needed."""
              if self.context:
                  try:
                      from serena.agent import SerenaAgentContext
                      # Map common aliases to actual context names
                      context_map = {
                          "claude-code": "desktop-app",  # Claude Code uses desktop-app context
                      }
                      context_name = context_map.get(self.context, self.context)
                      return SerenaAgentContext.from_name(context_name)
                  except Exception:
                      return None
              return None
      
          def _resolve_modes(self) -> Optional[List[Any]]:
              """Convert string modes to SerenaAgentMode list if needed."""
              if self.modes:
                  try:
                      from serena.agent import SerenaAgentMode
                      return [SerenaAgentMode(m) for m in self.modes]
                  except Exception:
                      return None
              return None
      
          def _ensure_agent(self) -> SerenaAgent:
              if self._agent is None:
                  config = SerenaConfig()
                  self._agent = SerenaAgent(
                      project=str(self.project_path),
                      serena_config=config,
                      context=self._resolve_context(),
                      modes=self._resolve_modes(),
                  )
              return self._agent
      
          def register_tool(self, tool: Any):
              """Register an extended tool locally in the wrapper."""
              tool_name = tool.name if hasattr(tool, 'name') else str(tool)
              self._extended_tools[tool_name] = tool
      
          def call_tool(self, name: str, **kwargs) -> Dict[str, Any]:
              """Execute a tool by name."""
      
              # Priority 1: Check extended tools managed by wrapper
              if name in self._extended_tools:
                  tool = self._extended_tools[name]
                  try:
                      if hasattr(tool, "_run"):
                          result = tool._run(**kwargs)
                      elif hasattr(tool, "run"):
                          result = tool.run(**kwargs)
                      else:
                          return {"error": {"code": "RUNTIME_ERROR", "message": f"Tool '{name}' has no recognized execution method"}}
                      return {"result": result}
                  except Exception as e:
                      return {"error": {"code": "RUNTIME_ERROR", "message": str(e)}}
      
              # Priority 2: Use ToolRegistry to find and execute core tools
              registry = ToolRegistry()
              if not registry.is_valid_tool_name(name):
                  return {"error": {"code": "TOOL_NOT_FOUND", "message": f"Tool '{name}' not found"}}
      
              try:
                  tool_class = registry.get_tool_class_by_name(name)
                  agent = self._ensure_agent()
                  tool_instance = agent.get_tool(tool_class)
      
                  if hasattr(tool_instance, "apply_ex"):
                      result = tool_instance.apply_ex(**kwargs)
                  else:
                      result = tool_instance.apply(**kwargs)
                  return {"result": result}
              except Exception as e:
                  return {"error": {"code": "RUNTIME_ERROR", "message": str(e)}}
      
          def list_tools(self, scope: str = "active") -> List[str]:
              """List available tools (Agent + Extended)."""
              tools = list(self._extended_tools.keys())
      
              try:
                  agent = self._ensure_agent()
                  if scope == "all":
                      registry = ToolRegistry()
                      tools.extend(registry.get_tool_names())
                  else:
                      tools.extend(agent.get_active_tool_names())
              except Exception:
                  pass
      
              return sorted(list(set(tools)))
      
          def get_active_context(self) -> str:
              """Get active context name from wrapper.
      
              Returns the user-configured context name (not the mapped internal name).
              """
              return self.context
      
          def get_active_modes(self) -> List[str]:
              """Get active mode names from wrapper configuration.
      
              Returns the user-configured modes (not filtered by agent).
              """
              return sorted(set(self.modes))
      
          def list_modes(self, scope: str = "all") -> List[str]:
              """List modes (active or all registered when discoverable)."""
              active_modes = self.get_active_modes()
              if scope == "active":
                  return active_modes
      
              try:
                  from serena.agent import SerenaAgentMode
                  all_modes = SerenaAgentMode.list_registered_mode_names()
                  return sorted(set(active_modes + all_modes))
              except Exception:
                  return active_modes
      
          def list_contexts(self, scope: str = "all") -> List[str]:
              """List contexts (active or all registered when discoverable)."""
              active_context = self.get_active_context()
              if scope == "active":
                  return [active_context] if active_context else []
      
              discovered: List[str] = []
              try:
                  from serena.agent import SerenaAgentContext
                  discovered = SerenaAgentContext.list_registered_context_names()
              except Exception:
                  discovered = []
      
              base_contexts = discovered if discovered else list(self.KNOWN_CONTEXTS)
              if active_context:
                  base_contexts.append(active_context)
              return sorted(set(base_contexts))
      
          def get_dashboard_info(self) -> Dict[str, Any]:
              """Get dashboard-like overview of current/available configuration."""
              active_tools = self.list_tools(scope="active")
              available_tools = self.list_tools(scope="all")
      
              active_project_path = str(self.project_path)
              try:
                  agent = self._ensure_agent()
                  if hasattr(agent, "get_active_project"):
                      project = agent.get_active_project()
                      if project is not None and hasattr(project, "project_root"):
                          active_project_path = str(Path(project.project_root).resolve())
              except Exception:
                  pass
      
              return {
                  "active_project_path": active_project_path,
                  "context": self.get_active_context(),
                  "active_modes": self.get_active_modes(),
                  "active_tools_count": len(active_tools),
                  "active_tools": active_tools,
                  "available_tools": available_tools,
                  "available_modes": self.list_modes(scope="all"),
                  "available_contexts": self.list_contexts(scope="all"),
              }
      
          def shutdown(self):
              """Cleanup resources."""
              if self._agent and hasattr(self._agent, "shutdown"):
                  self._agent.shutdown()
              self._agent = None
      
    • output.py 402 B
      """JSON output utilities for CLI."""
      import json
      import sys
      from typing import Any
      
      
      def output_json(data: Any, indent: int = 2) -> None:
          """Print JSON to stdout."""
          print(json.dumps(data, indent=indent, ensure_ascii=False))
      
      
      def output_error(code: str, message: str) -> None:
          """Print error JSON and exit."""
          output_json({"error": {"code": code, "message": message}})
          sys.exit(1)
      
    • paths.py 1.5 KB
      """Path resolution for Serena Tools configuration."""
      import os
      from pathlib import Path
      from typing import Optional
      
      
      class SerenaToolsPaths:
          """Manages configuration paths for Serena Tools wrapper."""
      
          def __init__(self):
              """Initialize path resolver."""
              pass
      
          def get_config_dir(self) -> Path:
              """Get configuration directory path.
      
              Returns ~/.serena or $SERENA_HOME if set.
              """
              serena_home = os.environ.get("SERENA_HOME")
              if serena_home:
                  return Path(serena_home)
              return Path.home() / ".serena"
      
          def get_config_file_path(self) -> Path:
              """Get configuration file path.
      
              Returns <config_dir>/serena_config.yml
              """
              return self.get_config_dir() / "serena_config.yml"
      
          def ensure_config_exists(self) -> Path:
              """Ensure configuration file exists, creating it if necessary.
      
              Returns path to config file.
              """
              config_file = self.get_config_file_path()
              config_dir = config_file.parent
      
              # Create directory if it doesn't exist
              config_dir.mkdir(parents=True, exist_ok=True)
      
              # Create default config if file doesn't exist
              if not config_file.exists():
                  default_config = """# Serena Tools Configuration
      # This file is auto-generated on first run
      
      projects: []
      web_dashboard:
        enabled: true
        host: "127.0.0.1"
        port: 8765
      log_level: "INFO"
      """
                  config_file.write_text(default_config)
      
              return config_file
      
    • __init__.py 310 B
      """Serena Tools Package."""
      
      from .core import SerenaCore
      from .extended.cmd_tools import RunCommandTool, RunScriptTool
      from .extended.config_tools import ReadConfigTool, UpdateConfigTool
      
      __all__ = [
          "SerenaCore",
          "RunCommandTool",
          "RunScriptTool",
          "ReadConfigTool",
          "UpdateConfigTool",
      ]
      
    • __main__.py 94 B
      """Entry point for Serena CLI."""
      from .cli import app
      
      if __name__ == "__main__":
          app()
      
      
  • .env.example 619 B · in bundle
  • README.md 6.8 KB
    # Serena CLI
    
    Semantic code understanding with IDE-like symbol operations and Web Dashboard. MCP-independent CLI for code navigation, editing, and project memory.
    
    ## Features
    
    - **Web Dashboard**: Real-time configuration monitoring and management
    - **Symbol Operations**: Find, rename, replace, insert symbols with language-aware precision
    - **Cross-file References**: Track symbol usages across entire codebase
    - **Project Memory**: Persist and retrieve project knowledge across sessions
    - **Extended Tools**: Shell commands, config file operations
    - **Cross-Drive Support**: Work with projects on any drive (Windows) or path (Unix)
    - **Auto Project Registration**: Automatically registers projects in global Serena config
    
    ## Installation
    
    ```bash
    pip install serena-agent typer pyyaml
    ```
    
    ## Quick Start
    
    **First-time setup**: Launch the Web Dashboard to initialize and register the project:
    
    ```bash
    # Start Web Dashboard (recommended for first-time use)
    python -m tools dashboard serve --open-browser
    
    # Or manually open browser after starting
    python -m tools dashboard serve
    # Then open: http://127.0.0.1:24282/dashboard/index.html
    ```
    
    **Configuration**: Create `.env` file in `skills/serena/` directory:
    
    ```bash
    SERENA_CONTEXT=claude-code
    SERENA_MODES=interactive,editing,onboarding
    SERENA_PROJECT=.
    SERENA_DASHBOARD_ENABLED=true
    SERENA_DASHBOARD_PORT=24282
    ```
    
    **Basic Usage**:
    
    ```bash
    # Find a symbol
    python -m tools symbol find MyClass --body
    
    # List symbols in file
    python -m tools symbol overview src/main.py
    
    # Find references
    python -m tools symbol refs MyClass/method
    
    # List available tools
    python -m tools workflow tools
    ```
    
    ## Command Structure
    
    ```bash
    python -m tools [GLOBAL OPTIONS] <command> [COMMAND OPTIONS]
    ```
    
    ### Global Options
    
    Global options must be specified **before** the command:
    
    - `-p, --project PATH` - Project directory (default: current directory, env: SERENA_PROJECT)
    - `-c, --context TEXT` - Execution context (auto-detected if not specified, env: SERENA_CONTEXT)
    - `-m, --mode TEXT` - Operation modes (can be specified multiple times, env: SERENA_MODES)
    
    ### Working with Different Projects
    
    **Important**: When working with projects in different locations (especially cross-drive on Windows), use `--project`:
    
    ```bash
    # Correct: Specify project path with --project
    python -m tools --project "E:\MyProject" file search "pattern"
    python -m tools --project "/home/user/project" symbol find MyClass
    python -m tools --project "D:\workspace\app" symbol refs MyClass
    
    # Incorrect: Don't use --path with absolute paths from different drives
    python -m tools file search "pattern" --path "E:\MyProject"  # Will fail!
    ```
    
    **Why?** The `--path` option in subcommands expects **relative paths** within the project. Always use `--project` to set the project root first.
    
    ## CLI Commands
    
    ### Dashboard Commands
    
    | Command | Description |
    |---------|-------------|
    | `dashboard serve [--open-browser] [--browser-cmd <path>]` | Start Web Dashboard server |
    | `dashboard info` | Show current configuration overview |
    | `dashboard tools` | List active and available tools |
    | `dashboard modes` | List active and available modes |
    | `dashboard contexts` | List active and available contexts |
    
    **Dashboard Options**:
    - `--open-browser` / `--no-open-browser`: Auto-open browser (default: False)
    - `--browser-cmd <path>`: Specify browser executable path
    - `--host <address>`: Listen address (default: 127.0.0.1)
    - `--port <number>`: Listen port (default: 24282, 0 for auto-select)
    - `SERENA_BROWSER_CMD`: Environment variable for browser command
    
    ### Symbol Operations
    
    | Command | Description |
    |---------|-------------|
    | `symbol find <name> [--body] [--depth N] [--path file]` | Find symbols by name |
    | `symbol overview <path>` | List all symbols in file |
    | `symbol refs <name> [--path file]` | Find symbol references |
    | `symbol replace <name> --path <file> --body <code>` | Replace symbol body |
    | `symbol insert-after <name> --path <file> --content <code>` | Insert after symbol |
    | `symbol insert-before <name> --path <file> --content <code>` | Insert before symbol |
    | `symbol rename <name> <new> --path <file>` | Rename symbol |
    
    ### Memory Operations
    
    | Command | Description |
    |---------|-------------|
    | `memory list` | List all memories |
    | `memory read <name>` | Read memory content |
    | `memory write <name> --content <text>` | Create/update memory |
    | `memory edit <name> --content <text>` | Edit memory |
    | `memory delete <name>` | Delete memory |
    
    ### File Operations
    
    | Command | Description |
    |---------|-------------|
    | `file list [--path <dir>] [--recursive]` | List directory contents |
    | `file find <pattern>` | Find files by glob pattern |
    | `file search <pattern> [--path <dir>]` | Search for regex pattern in files |
    
    ### Extended Tools
    
    | Command | Description |
    |---------|-------------|
    | `cmd run <cmd> [--cwd <dir>] [--timeout <N>]` | Execute shell command |
    | `cmd script <path> [--args "..."]` | Execute script file |
    | `config read <path> [--format json\|yaml]` | Read JSON/YAML config |
    | `config update <path> <key> <value>` | Update config value |
    
    ### Workflow
    
    | Command | Description |
    |---------|-------------|
    | `workflow onboarding` | Run project onboarding |
    | `workflow check` | Check onboarding status |
    | `workflow tools [--scope all]` | List available tools |
    
    ## Output Format
    
    All CLI output is JSON:
    
    ```json
    // Success
    {"result": <data>}
    
    // Error
    {"error": {"code": "ERROR_CODE", "message": "description"}}
    ```
    
    **Error codes**: `INVALID_ARGS`, `TOOL_NOT_FOUND`, `INIT_FAILED`, `RUNTIME_ERROR`
    
    ## Web Dashboard Features
    
    The Web Dashboard provides:
    - **Real-time Configuration**: View active context, modes, and tools
    - **Project Management**: See registered projects and active project
    - **Tool Monitoring**: Track active and available tools
    - **Configuration Editing**: Edit `.env` file directly from browser
    - **Auto Registration**: Automatically adds project to `~/.serena/serena_config.yml`
    
    Access the dashboard at: `http://127.0.0.1:24282/dashboard/index.html`
    
    ## Project Structure
    
    ```
    skills/serena/
    ├── SKILL.md              
    ├── README.md             
    ├── .env.example          
    └── tools/
        ├── core.py           # SerenaCore wrapper
        ├── paths.py          # Path utilities
        ├── output.py         # JSON output utilities
        ├── cli/              # Typer CLI commands
        │   ├── dashboard.py  
        │   ├── symbol.py     
        │   ├── memory.py     
        │   ├── file.py       
        │   ├── workflow.py   
        │   ├── cmd.py        
        │   └── config.py     
        ├── server/           # Web Dashboard server
        │   └── dashboard_server.py  # Flask HTTP server
        └── extended/         # Extended tools
            ├── cmd_tools.py
            └── config_tools.py
    ```
    
    ## License
    
    MIT
    
  • SKILL.md 7.1 KB
    ---
    name: serena
    description: |
      Semantic code understanding with IDE-like symbol operations. Use when: (1) Large codebase analysis (>50 files), (2) Symbol-level operations (find, rename, refactor), (3) Cross-file reference tracking, (4) Project memory and session persistence, (5) Multi-language semantic navigation. Triggers: "find symbol", "rename function", "find references", "symbol overview", "project memory". IMPORTANT: Prioritize Serena's symbolic tools over file-based grep/read for code exploration.
    ---
    
    # Serena - Semantic Code Understanding
    
    IDE-like semantic code operations via CLI. Provides symbol-level code navigation, editing, and project memory.
    
    ## Prerequisites
    
    ```bash
    pip install serena-agent typer pyyaml
    ```
    
    ## Quick Start
    
    **First-time setup**: Launch the Web Dashboard to initialize and register the project:
    
    ```bash
    python -m tools dashboard serve --open-browser
    ```
    
    This will:
    - Initialize Serena configuration
    - Register the current project in `~/.serena/serena_config.yml`
    - Open the Web Dashboard for monitoring and configuration
    
    **Configuration**: Edit `.env` file in `skills/serena/` directory:
    ```bash
    SERENA_CONTEXT=claude-code
    SERENA_MODES=interactive,editing,onboarding
    SERENA_PROJECT=.
    ```
    
    ## Usage
    
    ### Basic Command Structure
    
    ```bash
    python -m tools [GLOBAL OPTIONS] <command> [COMMAND OPTIONS]
    ```
    
    **Global Options** (must be specified before the command):
    - `-p, --project PATH` - Project directory (default: current directory, env: SERENA_PROJECT)
    - `-c, --context TEXT` - Execution context (auto-detected if not specified, env: SERENA_CONTEXT)
    - `-m, --mode TEXT` - Operation modes (can be specified multiple times, env: SERENA_MODES)
    
    ### Working with Different Projects
    
    **Important**: When working with projects in different locations (especially cross-drive on Windows), use `--project`:
    
    ```bash
    # Correct: Use --project for different project locations
    python -m tools --project "/path/to/project" symbol find MyClass
    python -m tools --project "E:\MyProject" file search "pattern"
    
    # Incorrect: Don't use --path with absolute paths from different drives
    python -m tools file search "pattern" --path "E:\MyProject"  # Will fail!
    ```
    
    The `--path` option in subcommands expects **relative paths** within the project. Always use `--project` to set the project root first.
    
    ### Common Operations
    
    ```bash
    # Dashboard
    python -m tools dashboard serve --open-browser
    python -m tools dashboard info
    
    # Symbol operations
    python -m tools symbol find MyClass --body
    python -m tools symbol overview src/main.py
    python -m tools symbol refs MyClass/method
    python -m tools symbol rename OldName NewName --path src/file.py
    
    # Memory operations
    python -m tools memory list
    python -m tools memory read project_overview
    python -m tools memory write api_notes --content "..."
    
    # File operations
    python -m tools file list --recursive
    python -m tools file find "**/*.py"
    python -m tools file search "TODO:.*" --path src
    
    # Extended tools
    python -m tools cmd run "git status"
    python -m tools config read config.json
    ```
    
    ## Tool Routing Policy
    
    ### Prefer Serena Over Built-in Tools
    
    | Task | Avoid | Use Serena CLI |
    |------|-------|----------------|
    | Find function | `grep "def func"` | `symbol find func --body` |
    | List file structure | `cat file.py` | `symbol overview file.py` |
    | Find usages | `grep "func("` | `symbol refs func` |
    | Edit function | `Edit` tool | `symbol replace func --path file.py` |
    | Rename | Manual find/replace | `symbol rename old new --path file.py` |
    
    ### When to Use Built-in Tools
    - Simple text search (non-code patterns)
    - Configuration files (JSON, YAML)
    - Documentation files (Markdown)
    
    ## Command Reference
    
    ### Dashboard Commands
    | Command | Description |
    |---------|-------------|
    | `dashboard serve [--open-browser] [--browser-cmd <path>]` | Start Web Dashboard server |
    | `dashboard info` | Show current configuration |
    | `dashboard tools` | List active and available tools |
    | `dashboard modes` | List active and available modes |
    | `dashboard contexts` | List active and available contexts |
    
    ### Symbol Commands
    | Command | Description |
    |---------|-------------|
    | `symbol find <name> [--body] [--depth N] [--path file]` | Find symbols by name |
    | `symbol overview <path>` | List all symbols in file |
    | `symbol refs <name> [--path file]` | Find symbol references |
    | `symbol replace <name> --path <file> --body <code>` | Replace symbol body |
    | `symbol insert-after <name> --path <file> --content <code>` | Insert after symbol |
    | `symbol insert-before <name> --path <file> --content <code>` | Insert before symbol |
    | `symbol rename <name> <new> --path <file>` | Rename symbol |
    
    ### Memory Commands
    | Command | Description |
    |---------|-------------|
    | `memory list` | List all memories |
    | `memory read <name>` | Read memory content |
    | `memory write <name> --content <text>` | Create/update memory |
    | `memory edit <name> --content <text>` | Edit memory |
    | `memory delete <name>` | Delete memory |
    
    ### File Commands
    | Command | Description |
    |---------|-------------|
    | `file list [--path dir] [--recursive]` | List directory |
    | `file find <pattern>` | Find files by glob pattern |
    | `file search <pattern> [--path dir]` | Search for regex pattern |
    
    ### Extended Commands
    | Command | Description |
    |---------|-------------|
    | `cmd run <command> [--cwd dir] [--timeout N]` | Execute shell command |
    | `cmd script <path> [--args "..."]` | Execute script file |
    | `config read <path> [--format json\|yaml]` | Read config file |
    | `config update <path> <key> <value>` | Update config value |
    
    ### Workflow Commands
    | Command | Description |
    |---------|-------------|
    | `workflow onboarding` | Run project onboarding |
    | `workflow check` | Check onboarding status |
    | `workflow tools [--scope all]` | List available tools |
    
    ## Workflow Examples
    
    ### Phase 1: Exploration
    ```bash
    python -m tools symbol overview src/main.py           # Understand file structure
    python -m tools symbol find MyClass --depth 1         # Explore class members
    python -m tools symbol find MyClass/method --body     # Get implementation details
    ```
    
    ### Phase 2: Analysis
    ```bash
    python -m tools symbol refs MyClass/method            # Impact analysis
    python -m tools memory list                           # Check project knowledge
    python -m tools memory read architecture              # Retrieve context
    ```
    
    ### Phase 3: Modification
    ```bash
    python -m tools symbol find target --body             # Verify target
    python -m tools symbol replace target --path f --body "..."  # Edit
    python -m tools symbol rename old new --path f        # Refactor
    ```
    
    ## Error Handling
    
    All CLI output is JSON:
    
    ```json
    // Success
    {"result": <data>}
    
    // Error
    {"error": {"code": "ERROR_CODE", "message": "description"}}
    ```
    
    | Error Code | Recovery |
    |------------|----------|
    | `INVALID_ARGS` | Check `--help` |
    | `TOOL_NOT_FOUND` | Use `workflow tools` |
    | `INIT_FAILED` | Check serena-agent installation |
    | `RUNTIME_ERROR` | Check error message |
    
    ## Anti-Patterns
    
    | Prohibited | Correct |
    |------------|---------|
    | Read entire file to find function | `symbol find func --body` |
    | Grep for function calls | `symbol refs func` |
    | Manual search-replace rename | `symbol rename old new --path f` |
    | Skip impact analysis | `symbol refs` before editing |
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related