#
tokens: 48717/50000 46/294 files (page 2/14)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 14. Use http://codebase.md/oraios/serena?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .devcontainer
│   └── devcontainer.json
├── .dockerignore
├── .env.example
├── .github
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── config.yml
│   │   ├── feature_request.md
│   │   └── issue--bug--performance-problem--question-.md
│   └── workflows
│       ├── codespell.yml
│       ├── docker.yml
│       ├── junie.yml
│       ├── lint_and_docs.yaml
│       ├── publish.yml
│       └── pytest.yml
├── .gitignore
├── .serena
│   ├── memories
│   │   ├── adding_new_language_support_guide.md
│   │   ├── serena_core_concepts_and_architecture.md
│   │   ├── serena_repository_structure.md
│   │   └── suggested_commands.md
│   └── project.yml
├── .vscode
│   └── settings.json
├── CHANGELOG.md
├── CLAUDE.md
├── compose.yaml
├── CONTRIBUTING.md
├── docker_build_and_run.sh
├── DOCKER.md
├── Dockerfile
├── docs
│   ├── custom_agent.md
│   └── serena_on_chatgpt.md
├── flake.lock
├── flake.nix
├── lessons_learned.md
├── LICENSE
├── llms-install.md
├── public
│   └── .gitignore
├── pyproject.toml
├── README.md
├── resources
│   ├── serena-icons.cdr
│   ├── serena-logo-dark-mode.svg
│   ├── serena-logo.cdr
│   ├── serena-logo.svg
│   └── vscode_sponsor_logo.png
├── roadmap.md
├── scripts
│   ├── agno_agent.py
│   ├── demo_run_tools.py
│   ├── gen_prompt_factory.py
│   ├── mcp_server.py
│   ├── print_mode_context_options.py
│   └── print_tool_overview.py
├── src
│   ├── interprompt
│   │   ├── __init__.py
│   │   ├── .syncCommitId.remote
│   │   ├── .syncCommitId.this
│   │   ├── jinja_template.py
│   │   ├── multilang_prompt.py
│   │   ├── prompt_factory.py
│   │   └── util
│   │       ├── __init__.py
│   │       └── class_decorators.py
│   ├── README.md
│   ├── serena
│   │   ├── __init__.py
│   │   ├── agent.py
│   │   ├── agno.py
│   │   ├── analytics.py
│   │   ├── cli.py
│   │   ├── code_editor.py
│   │   ├── config
│   │   │   ├── __init__.py
│   │   │   ├── context_mode.py
│   │   │   └── serena_config.py
│   │   ├── constants.py
│   │   ├── dashboard.py
│   │   ├── generated
│   │   │   └── generated_prompt_factory.py
│   │   ├── gui_log_viewer.py
│   │   ├── mcp.py
│   │   ├── project.py
│   │   ├── prompt_factory.py
│   │   ├── resources
│   │   │   ├── config
│   │   │   │   ├── contexts
│   │   │   │   │   ├── agent.yml
│   │   │   │   │   ├── chatgpt.yml
│   │   │   │   │   ├── codex.yml
│   │   │   │   │   ├── context.template.yml
│   │   │   │   │   ├── desktop-app.yml
│   │   │   │   │   ├── ide-assistant.yml
│   │   │   │   │   └── oaicompat-agent.yml
│   │   │   │   ├── internal_modes
│   │   │   │   │   └── jetbrains.yml
│   │   │   │   ├── modes
│   │   │   │   │   ├── editing.yml
│   │   │   │   │   ├── interactive.yml
│   │   │   │   │   ├── mode.template.yml
│   │   │   │   │   ├── no-onboarding.yml
│   │   │   │   │   ├── onboarding.yml
│   │   │   │   │   ├── one-shot.yml
│   │   │   │   │   └── planning.yml
│   │   │   │   └── prompt_templates
│   │   │   │       ├── simple_tool_outputs.yml
│   │   │   │       └── system_prompt.yml
│   │   │   ├── dashboard
│   │   │   │   ├── dashboard.js
│   │   │   │   ├── index.html
│   │   │   │   ├── jquery.min.js
│   │   │   │   ├── serena-icon-16.png
│   │   │   │   ├── serena-icon-32.png
│   │   │   │   ├── serena-icon-48.png
│   │   │   │   ├── serena-logs-dark-mode.png
│   │   │   │   └── serena-logs.png
│   │   │   ├── project.template.yml
│   │   │   └── serena_config.template.yml
│   │   ├── symbol.py
│   │   ├── text_utils.py
│   │   ├── tools
│   │   │   ├── __init__.py
│   │   │   ├── cmd_tools.py
│   │   │   ├── config_tools.py
│   │   │   ├── file_tools.py
│   │   │   ├── jetbrains_plugin_client.py
│   │   │   ├── jetbrains_tools.py
│   │   │   ├── memory_tools.py
│   │   │   ├── symbol_tools.py
│   │   │   ├── tools_base.py
│   │   │   └── workflow_tools.py
│   │   └── util
│   │       ├── class_decorators.py
│   │       ├── exception.py
│   │       ├── file_system.py
│   │       ├── general.py
│   │       ├── git.py
│   │       ├── inspection.py
│   │       ├── logging.py
│   │       ├── shell.py
│   │       └── thread.py
│   └── solidlsp
│       ├── __init__.py
│       ├── .gitignore
│       ├── language_servers
│       │   ├── al_language_server.py
│       │   ├── bash_language_server.py
│       │   ├── clangd_language_server.py
│       │   ├── clojure_lsp.py
│       │   ├── common.py
│       │   ├── csharp_language_server.py
│       │   ├── dart_language_server.py
│       │   ├── eclipse_jdtls.py
│       │   ├── elixir_tools
│       │   │   ├── __init__.py
│       │   │   ├── elixir_tools.py
│       │   │   └── README.md
│       │   ├── elm_language_server.py
│       │   ├── erlang_language_server.py
│       │   ├── gopls.py
│       │   ├── intelephense.py
│       │   ├── jedi_server.py
│       │   ├── kotlin_language_server.py
│       │   ├── lua_ls.py
│       │   ├── marksman.py
│       │   ├── nixd_ls.py
│       │   ├── omnisharp
│       │   │   ├── initialize_params.json
│       │   │   ├── runtime_dependencies.json
│       │   │   └── workspace_did_change_configuration.json
│       │   ├── omnisharp.py
│       │   ├── perl_language_server.py
│       │   ├── pyright_server.py
│       │   ├── r_language_server.py
│       │   ├── ruby_lsp.py
│       │   ├── rust_analyzer.py
│       │   ├── solargraph.py
│       │   ├── sourcekit_lsp.py
│       │   ├── terraform_ls.py
│       │   ├── typescript_language_server.py
│       │   ├── vts_language_server.py
│       │   └── zls.py
│       ├── ls_config.py
│       ├── ls_exceptions.py
│       ├── ls_handler.py
│       ├── ls_logger.py
│       ├── ls_request.py
│       ├── ls_types.py
│       ├── ls_utils.py
│       ├── ls.py
│       ├── lsp_protocol_handler
│       │   ├── lsp_constants.py
│       │   ├── lsp_requests.py
│       │   ├── lsp_types.py
│       │   └── server.py
│       ├── settings.py
│       └── util
│           ├── subprocess_util.py
│           └── zip.py
├── test
│   ├── __init__.py
│   ├── conftest.py
│   ├── resources
│   │   └── repos
│   │       ├── al
│   │       │   └── test_repo
│   │       │       ├── app.json
│   │       │       └── src
│   │       │           ├── Codeunits
│   │       │           │   ├── CustomerMgt.Codeunit.al
│   │       │           │   └── PaymentProcessorImpl.Codeunit.al
│   │       │           ├── Enums
│   │       │           │   └── CustomerType.Enum.al
│   │       │           ├── Interfaces
│   │       │           │   └── IPaymentProcessor.Interface.al
│   │       │           ├── Pages
│   │       │           │   ├── CustomerCard.Page.al
│   │       │           │   └── CustomerList.Page.al
│   │       │           ├── TableExtensions
│   │       │           │   └── Item.TableExt.al
│   │       │           └── Tables
│   │       │               └── Customer.Table.al
│   │       ├── bash
│   │       │   └── test_repo
│   │       │       ├── config.sh
│   │       │       ├── main.sh
│   │       │       └── utils.sh
│   │       ├── clojure
│   │       │   └── test_repo
│   │       │       ├── deps.edn
│   │       │       └── src
│   │       │           └── test_app
│   │       │               ├── core.clj
│   │       │               └── utils.clj
│   │       ├── csharp
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── Models
│   │       │       │   └── Person.cs
│   │       │       ├── Program.cs
│   │       │       ├── serena.sln
│   │       │       └── TestProject.csproj
│   │       ├── dart
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── lib
│   │       │       │   ├── helper.dart
│   │       │       │   ├── main.dart
│   │       │       │   └── models.dart
│   │       │       └── pubspec.yaml
│   │       ├── elixir
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── lib
│   │       │       │   ├── examples.ex
│   │       │       │   ├── ignored_dir
│   │       │       │   │   └── ignored_module.ex
│   │       │       │   ├── models.ex
│   │       │       │   ├── services.ex
│   │       │       │   ├── test_repo.ex
│   │       │       │   └── utils.ex
│   │       │       ├── mix.exs
│   │       │       ├── mix.lock
│   │       │       ├── scripts
│   │       │       │   └── build_script.ex
│   │       │       └── test
│   │       │           ├── models_test.exs
│   │       │           └── test_repo_test.exs
│   │       ├── elm
│   │       │   └── test_repo
│   │       │       ├── elm.json
│   │       │       ├── Main.elm
│   │       │       └── Utils.elm
│   │       ├── erlang
│   │       │   └── test_repo
│   │       │       ├── hello.erl
│   │       │       ├── ignored_dir
│   │       │       │   └── ignored_module.erl
│   │       │       ├── include
│   │       │       │   ├── records.hrl
│   │       │       │   └── types.hrl
│   │       │       ├── math_utils.erl
│   │       │       ├── rebar.config
│   │       │       ├── src
│   │       │       │   ├── app.erl
│   │       │       │   ├── models.erl
│   │       │       │   ├── services.erl
│   │       │       │   └── utils.erl
│   │       │       └── test
│   │       │           ├── models_tests.erl
│   │       │           └── utils_tests.erl
│   │       ├── go
│   │       │   └── test_repo
│   │       │       └── main.go
│   │       ├── java
│   │       │   └── test_repo
│   │       │       ├── pom.xml
│   │       │       └── src
│   │       │           └── main
│   │       │               └── java
│   │       │                   └── test_repo
│   │       │                       ├── Main.java
│   │       │                       ├── Model.java
│   │       │                       ├── ModelUser.java
│   │       │                       └── Utils.java
│   │       ├── kotlin
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── build.gradle.kts
│   │       │       └── src
│   │       │           └── main
│   │       │               └── kotlin
│   │       │                   └── test_repo
│   │       │                       ├── Main.kt
│   │       │                       ├── Model.kt
│   │       │                       ├── ModelUser.kt
│   │       │                       └── Utils.kt
│   │       ├── lua
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── main.lua
│   │       │       ├── src
│   │       │       │   ├── calculator.lua
│   │       │       │   └── utils.lua
│   │       │       └── tests
│   │       │           └── test_calculator.lua
│   │       ├── markdown
│   │       │   └── test_repo
│   │       │       ├── api.md
│   │       │       ├── CONTRIBUTING.md
│   │       │       ├── guide.md
│   │       │       └── README.md
│   │       ├── nix
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── default.nix
│   │       │       ├── flake.nix
│   │       │       ├── lib
│   │       │       │   └── utils.nix
│   │       │       ├── modules
│   │       │       │   └── example.nix
│   │       │       └── scripts
│   │       │           └── hello.sh
│   │       ├── perl
│   │       │   └── test_repo
│   │       │       ├── helper.pl
│   │       │       └── main.pl
│   │       ├── php
│   │       │   └── test_repo
│   │       │       ├── helper.php
│   │       │       ├── index.php
│   │       │       └── simple_var.php
│   │       ├── python
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── custom_test
│   │       │       │   ├── __init__.py
│   │       │       │   └── advanced_features.py
│   │       │       ├── examples
│   │       │       │   ├── __init__.py
│   │       │       │   └── user_management.py
│   │       │       ├── ignore_this_dir_with_postfix
│   │       │       │   └── ignored_module.py
│   │       │       ├── scripts
│   │       │       │   ├── __init__.py
│   │       │       │   └── run_app.py
│   │       │       └── test_repo
│   │       │           ├── __init__.py
│   │       │           ├── complex_types.py
│   │       │           ├── models.py
│   │       │           ├── name_collisions.py
│   │       │           ├── nested_base.py
│   │       │           ├── nested.py
│   │       │           ├── overloaded.py
│   │       │           ├── services.py
│   │       │           ├── utils.py
│   │       │           └── variables.py
│   │       ├── r
│   │       │   └── test_repo
│   │       │       ├── .Rbuildignore
│   │       │       ├── DESCRIPTION
│   │       │       ├── examples
│   │       │       │   └── analysis.R
│   │       │       ├── NAMESPACE
│   │       │       └── R
│   │       │           ├── models.R
│   │       │           └── utils.R
│   │       ├── ruby
│   │       │   └── test_repo
│   │       │       ├── .solargraph.yml
│   │       │       ├── examples
│   │       │       │   └── user_management.rb
│   │       │       ├── lib.rb
│   │       │       ├── main.rb
│   │       │       ├── models.rb
│   │       │       ├── nested.rb
│   │       │       ├── services.rb
│   │       │       └── variables.rb
│   │       ├── rust
│   │       │   ├── test_repo
│   │       │   │   ├── Cargo.lock
│   │       │   │   ├── Cargo.toml
│   │       │   │   └── src
│   │       │   │       ├── lib.rs
│   │       │   │       └── main.rs
│   │       │   └── test_repo_2024
│   │       │       ├── Cargo.lock
│   │       │       ├── Cargo.toml
│   │       │       └── src
│   │       │           ├── lib.rs
│   │       │           └── main.rs
│   │       ├── swift
│   │       │   └── test_repo
│   │       │       ├── Package.swift
│   │       │       └── src
│   │       │           ├── main.swift
│   │       │           └── utils.swift
│   │       ├── terraform
│   │       │   └── test_repo
│   │       │       ├── data.tf
│   │       │       ├── main.tf
│   │       │       ├── outputs.tf
│   │       │       └── variables.tf
│   │       ├── typescript
│   │       │   └── test_repo
│   │       │       ├── .serena
│   │       │       │   └── project.yml
│   │       │       ├── index.ts
│   │       │       ├── tsconfig.json
│   │       │       └── use_helper.ts
│   │       └── zig
│   │           └── test_repo
│   │               ├── .gitignore
│   │               ├── build.zig
│   │               ├── src
│   │               │   ├── calculator.zig
│   │               │   ├── main.zig
│   │               │   └── math_utils.zig
│   │               └── zls.json
│   ├── serena
│   │   ├── __init__.py
│   │   ├── __snapshots__
│   │   │   └── test_symbol_editing.ambr
│   │   ├── config
│   │   │   ├── __init__.py
│   │   │   └── test_serena_config.py
│   │   ├── test_edit_marker.py
│   │   ├── test_mcp.py
│   │   ├── test_serena_agent.py
│   │   ├── test_symbol_editing.py
│   │   ├── test_symbol.py
│   │   ├── test_text_utils.py
│   │   ├── test_tool_parameter_types.py
│   │   └── util
│   │       ├── test_exception.py
│   │       └── test_file_system.py
│   └── solidlsp
│       ├── al
│       │   └── test_al_basic.py
│       ├── bash
│       │   ├── __init__.py
│       │   └── test_bash_basic.py
│       ├── clojure
│       │   ├── __init__.py
│       │   └── test_clojure_basic.py
│       ├── csharp
│       │   └── test_csharp_basic.py
│       ├── dart
│       │   ├── __init__.py
│       │   └── test_dart_basic.py
│       ├── elixir
│       │   ├── __init__.py
│       │   ├── conftest.py
│       │   ├── test_elixir_basic.py
│       │   ├── test_elixir_ignored_dirs.py
│       │   ├── test_elixir_integration.py
│       │   └── test_elixir_symbol_retrieval.py
│       ├── elm
│       │   └── test_elm_basic.py
│       ├── erlang
│       │   ├── __init__.py
│       │   ├── conftest.py
│       │   ├── test_erlang_basic.py
│       │   ├── test_erlang_ignored_dirs.py
│       │   └── test_erlang_symbol_retrieval.py
│       ├── go
│       │   └── test_go_basic.py
│       ├── java
│       │   └── test_java_basic.py
│       ├── kotlin
│       │   └── test_kotlin_basic.py
│       ├── lua
│       │   └── test_lua_basic.py
│       ├── markdown
│       │   ├── __init__.py
│       │   └── test_markdown_basic.py
│       ├── nix
│       │   └── test_nix_basic.py
│       ├── perl
│       │   └── test_perl_basic.py
│       ├── php
│       │   └── test_php_basic.py
│       ├── python
│       │   ├── test_python_basic.py
│       │   ├── test_retrieval_with_ignored_dirs.py
│       │   └── test_symbol_retrieval.py
│       ├── r
│       │   ├── __init__.py
│       │   └── test_r_basic.py
│       ├── ruby
│       │   ├── test_ruby_basic.py
│       │   └── test_ruby_symbol_retrieval.py
│       ├── rust
│       │   ├── test_rust_2024_edition.py
│       │   └── test_rust_basic.py
│       ├── swift
│       │   └── test_swift_basic.py
│       ├── terraform
│       │   └── test_terraform_basic.py
│       ├── typescript
│       │   └── test_typescript_basic.py
│       ├── util
│       │   └── test_zip.py
│       └── zig
│           └── test_zig_basic.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/test/resources/repos/python/test_repo/test_repo/nested_base.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Module to test parsing of classes with nested module paths in base classes.
 3 | """
 4 | 
 5 | from typing import Generic, TypeVar
 6 | 
 7 | T = TypeVar("T")
 8 | 
 9 | 
10 | class BaseModule:
11 |     """Base module class for nested module tests."""
12 | 
13 | 
14 | class SubModule:
15 |     """Sub-module class for nested paths."""
16 | 
17 |     class NestedBase:
18 |         """Nested base class."""
19 | 
20 |         def base_method(self):
21 |             """Base method."""
22 |             return "base"
23 | 
24 |         class NestedLevel2:
25 |             """Nested level 2."""
26 | 
27 |             def nested_level_2_method(self):
28 |                 """Nested level 2 method."""
29 |                 return "nested_level_2"
30 | 
31 |     class GenericBase(Generic[T]):
32 |         """Generic nested base class."""
33 | 
34 |         def generic_method(self, value: T) -> T:
35 |             """Generic method."""
36 |             return value
37 | 
38 | 
39 | # Classes extending base classes with single-level nesting
40 | class FirstLevel(SubModule):
41 |     """Class extending a class from a nested module path."""
42 | 
43 |     def first_level_method(self):
44 |         """First level method."""
45 |         return "first"
46 | 
47 | 
48 | # Classes extending base classes with multi-level nesting
49 | class TwoLevel(SubModule.NestedBase):
50 |     """Class extending a doubly-nested base class."""
51 | 
52 |     def multi_level_method(self):
53 |         """Multi-level method."""
54 |         return "multi"
55 | 
56 |     def base_method(self):
57 |         """Override of base method."""
58 |         return "overridden"
59 | 
60 | 
61 | class ThreeLevel(SubModule.NestedBase.NestedLevel2):
62 |     """Class extending a triply-nested base class."""
63 | 
64 |     def three_level_method(self):
65 |         """Three-level method."""
66 |         return "three"
67 | 
68 | 
69 | # Class extending a generic base class with nesting
70 | class GenericExtension(SubModule.GenericBase[str]):
71 |     """Class extending a generic nested base class."""
72 | 
73 |     def generic_extension_method(self, text: str) -> str:
74 |         """Extension method."""
75 |         return f"Extended: {text}"
76 | 
```

--------------------------------------------------------------------------------
/src/serena/util/inspection.py:
--------------------------------------------------------------------------------

```python
 1 | import logging
 2 | import os
 3 | from collections.abc import Generator
 4 | from typing import TypeVar
 5 | 
 6 | from serena.util.file_system import find_all_non_ignored_files
 7 | from solidlsp.ls_config import Language
 8 | 
 9 | T = TypeVar("T")
10 | 
11 | log = logging.getLogger(__name__)
12 | 
13 | 
14 | def iter_subclasses(cls: type[T], recursive: bool = True) -> Generator[type[T], None, None]:
15 |     """Iterate over all subclasses of a class. If recursive is True, also iterate over all subclasses of all subclasses."""
16 |     for subclass in cls.__subclasses__():
17 |         yield subclass
18 |         if recursive:
19 |             yield from iter_subclasses(subclass, recursive)
20 | 
21 | 
22 | def determine_programming_language_composition(repo_path: str) -> dict[str, float]:
23 |     """
24 |     Determine the programming language composition of a repository.
25 | 
26 |     :param repo_path: Path to the repository to analyze
27 | 
28 |     :return: Dictionary mapping language names to percentages of files matching each language
29 |     """
30 |     all_files = find_all_non_ignored_files(repo_path)
31 | 
32 |     if not all_files:
33 |         return {}
34 | 
35 |     # Count files for each language
36 |     language_counts: dict[str, int] = {}
37 |     total_files = len(all_files)
38 | 
39 |     for language in Language.iter_all(include_experimental=False):
40 |         matcher = language.get_source_fn_matcher()
41 |         count = 0
42 | 
43 |         for file_path in all_files:
44 |             # Use just the filename for matching, not the full path
45 |             filename = os.path.basename(file_path)
46 |             if matcher.is_relevant_filename(filename):
47 |                 count += 1
48 | 
49 |         if count > 0:
50 |             language_counts[str(language)] = count
51 | 
52 |     # Convert counts to percentages
53 |     language_percentages: dict[str, float] = {}
54 |     for language_name, count in language_counts.items():
55 |         percentage = (count / total_files) * 100
56 |         language_percentages[language_name] = round(percentage, 2)
57 | 
58 |     return language_percentages
59 | 
```

--------------------------------------------------------------------------------
/src/solidlsp/ls_logger.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Multilspy logger module.
 3 | """
 4 | 
 5 | import inspect
 6 | import logging
 7 | from datetime import datetime
 8 | 
 9 | from pydantic import BaseModel
10 | 
11 | 
12 | class LogLine(BaseModel):
13 |     """
14 |     Represents a line in the Multilspy log
15 |     """
16 | 
17 |     time: str
18 |     level: str
19 |     caller_file: str
20 |     caller_name: str
21 |     caller_line: int
22 |     message: str
23 | 
24 | 
25 | class LanguageServerLogger:
26 |     """
27 |     Logger class
28 |     """
29 | 
30 |     def __init__(self, json_format: bool = False, log_level: int = logging.INFO) -> None:
31 |         self.logger = logging.getLogger("solidlsp")
32 |         self.logger.setLevel(log_level)
33 |         self.json_format = json_format
34 | 
35 |     def log(self, debug_message: str, level: int, sanitized_error_message: str = "", stacklevel: int = 2) -> None:
36 |         """
37 |         Log the debug and sanitized messages using the logger
38 |         """
39 |         debug_message = debug_message.replace("'", '"').replace("\n", " ")
40 |         sanitized_error_message = sanitized_error_message.replace("'", '"').replace("\n", " ")
41 | 
42 |         # Collect details about the callee
43 |         curframe = inspect.currentframe()
44 |         calframe = inspect.getouterframes(curframe, 2)
45 |         caller_file = calframe[1][1].split("/")[-1]
46 |         caller_line = calframe[1][2]
47 |         caller_name = calframe[1][3]
48 | 
49 |         if self.json_format:
50 |             # Construct the debug log line
51 |             debug_log_line = LogLine(
52 |                 time=str(datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
53 |                 level=logging.getLevelName(level),
54 |                 caller_file=caller_file,
55 |                 caller_name=caller_name,
56 |                 caller_line=caller_line,
57 |                 message=debug_message,
58 |             )
59 | 
60 |             self.logger.log(
61 |                 level=level,
62 |                 msg=debug_log_line.json(),
63 |                 stacklevel=stacklevel,
64 |             )
65 |         else:
66 |             self.logger.log(level, debug_message, stacklevel=stacklevel)
67 | 
```

--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Build and Push Docker Images
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [ main ]
 6 |     tags: [ 'v*' ]
 7 | 
 8 | env:
 9 |   REGISTRY: ghcr.io
10 |   IMAGE_NAME: ${{ github.repository }}
11 | 
12 | jobs:
13 |   build-and-push:
14 |     runs-on: ubuntu-latest
15 |     timeout-minutes: 15
16 |     permissions:
17 |       contents: read
18 |       packages: write
19 | 
20 |     steps:
21 |     - name: Checkout repository
22 |       uses: actions/checkout@v4
23 | 
24 |     - name: Set up Docker Buildx
25 |       uses: docker/setup-buildx-action@v3
26 | 
27 |     - name: Log in to Container Registry
28 |       if: github.event_name != 'pull_request'
29 |       uses: docker/login-action@v3
30 |       with:
31 |         registry: ${{ env.REGISTRY }}
32 |         username: ${{ github.actor }}
33 |         password: ${{ secrets.GITHUB_TOKEN }}
34 | 
35 |     - name: Extract metadata
36 |       id: meta
37 |       uses: docker/metadata-action@v5
38 |       with:
39 |         images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
40 |         tags: |
41 |           type=ref,event=branch
42 |           type=ref,event=pr
43 |           type=semver,pattern={{version}}
44 |           type=semver,pattern={{major}}.{{minor}}
45 |           type=raw,value=latest,enable={{is_default_branch}}
46 | 
47 |     - name: Build and push production image
48 |       uses: docker/build-push-action@v5
49 |       with:
50 |         context: .
51 |         target: production
52 |         platforms: linux/amd64,linux/arm64
53 |         push: ${{ github.event_name != 'pull_request' }}
54 |         tags: ${{ steps.meta.outputs.tags }}
55 |         labels: ${{ steps.meta.outputs.labels }}
56 |         cache-from: type=gha
57 |         cache-to: type=gha,mode=max
58 | 
59 |     - name: Build and push development image
60 |       uses: docker/build-push-action@v5
61 |       with:
62 |         context: .
63 |         target: development
64 |         platforms: linux/amd64,linux/arm64
65 |         push: ${{ github.event_name != 'pull_request' }}
66 |         tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
67 |         labels: ${{ steps.meta.outputs.labels }}
68 |         cache-from: type=gha
69 |         cache-to: type=gha,mode=max
```

--------------------------------------------------------------------------------
/src/solidlsp/lsp_protocol_handler/lsp_constants.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | This module contains constants used in the LSP protocol.
 3 | """
 4 | 
 5 | 
 6 | class LSPConstants:
 7 |     """
 8 |     This class contains constants used in the LSP protocol.
 9 |     """
10 | 
11 |     # the key for uri used to represent paths
12 |     URI = "uri"
13 | 
14 |     # the key for range, which is a from and to position within a text document
15 |     RANGE = "range"
16 | 
17 |     # A key used in LocationLink type, used as the span of the origin link
18 |     ORIGIN_SELECTION_RANGE = "originSelectionRange"
19 | 
20 |     # A key used in LocationLink type, used as the target uri of the link
21 |     TARGET_URI = "targetUri"
22 | 
23 |     # A key used in LocationLink type, used as the target range of the link
24 |     TARGET_RANGE = "targetRange"
25 | 
26 |     # A key used in LocationLink type, used as the target selection range of the link
27 |     TARGET_SELECTION_RANGE = "targetSelectionRange"
28 | 
29 |     # key for the textDocument field in the request
30 |     TEXT_DOCUMENT = "textDocument"
31 | 
32 |     # key used to represent the language a document is in - "java", "csharp", etc.
33 |     LANGUAGE_ID = "languageId"
34 | 
35 |     # key used to represent the version of a document (a shared value between the client and server)
36 |     VERSION = "version"
37 | 
38 |     # key used to represent the text of a document being sent from the client to the server on open
39 |     TEXT = "text"
40 | 
41 |     # key used to represent a position (line and colnum) within a text document
42 |     POSITION = "position"
43 | 
44 |     # key used to represent the line number of a position
45 |     LINE = "line"
46 | 
47 |     # key used to represent the column number of a position
48 |     CHARACTER = "character"
49 | 
50 |     # key used to represent the changes made to a document
51 |     CONTENT_CHANGES = "contentChanges"
52 | 
53 |     # key used to represent name of symbols
54 |     NAME = "name"
55 | 
56 |     # key used to represent the kind of symbols
57 |     KIND = "kind"
58 | 
59 |     # key used to represent children in document symbols
60 |     CHILDREN = "children"
61 | 
62 |     # key used to represent the location in symbols
63 |     LOCATION = "location"
64 | 
65 |     # Severity level of the diagnostic
66 |     SEVERITY = "severity"
67 | 
68 |     # The message of the diagnostic
69 |     MESSAGE = "message"
70 | 
```

--------------------------------------------------------------------------------
/src/serena/resources/config/contexts/chatgpt.yml:
--------------------------------------------------------------------------------

```yaml
 1 | description: A configuration specific for chatgpt, which has a limit of 30 tools and requires short descriptions.
 2 | prompt: |
 3 |   You are running in desktop app context where the tools give you access to the code base as well as some
 4 |   access to the file system, if configured. You interact with the user through a chat interface that is separated
 5 |   from the code base. As a consequence, if you are in interactive mode, your communication with the user should
 6 |   involve high-level thinking and planning as well as some summarization of any code edits that you make.
 7 |   For viewing the code edits the user will view them in a separate code editor window, and the back-and-forth
 8 |   between the chat and the code editor should be minimized as well as facilitated by you.
 9 |   If complex changes have been made, advise the user on how to review them in the code editor.
10 |   If complex relationships that the user asked for should be visualized or explained, consider creating
11 |   a diagram in addition to your text-based communication. Note that in the chat interface you have various rendering
12 |   options for text, html, and mermaid diagrams, as has been explained to you in your initial instructions.
13 | excluded_tools: []
14 | included_optional_tools:
15 |   - switch_modes
16 | 
17 | tool_description_overrides:
18 |   find_symbol: |
19 |     Retrieves symbols matching `name_path` in a file.
20 |     Use `depth > 0` to include children. `name_path` can be: "foo": any symbol named "foo"; "foo/bar": "bar" within "foo"; "/foo/bar": only top-level "foo/bar"
21 |   replace_regex: |
22 |     Replaces text using regular expressions. Preferred for smaller edits where symbol-level tools aren't appropriate.
23 |     Use wildcards (.*?) to match large sections efficiently: "beginning.*?end" instead of specifying exact content.
24 |     Essential for multi-line replacements.
25 |   search_for_pattern: |
26 |     Flexible pattern search across codebase. Prefer symbolic operations when possible.
27 |     Uses DOTALL matching. Use non-greedy quantifiers (.*?) to avoid over-matching.
28 |     Supports file filtering via globs and code-only restriction.
```

--------------------------------------------------------------------------------
/src/serena/tools/cmd_tools.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Tools supporting the execution of (external) commands
 3 | """
 4 | 
 5 | import os.path
 6 | 
 7 | from serena.tools import Tool, ToolMarkerCanEdit
 8 | from serena.util.shell import execute_shell_command
 9 | 
10 | 
11 | class ExecuteShellCommandTool(Tool, ToolMarkerCanEdit):
12 |     """
13 |     Executes a shell command.
14 |     """
15 | 
16 |     def apply(
17 |         self,
18 |         command: str,
19 |         cwd: str | None = None,
20 |         capture_stderr: bool = True,
21 |         max_answer_chars: int = -1,
22 |     ) -> str:
23 |         """
24 |         Execute a shell command and return its output. If there is a memory about suggested commands, read that first.
25 |         Never execute unsafe shell commands!
26 |         IMPORTANT: Do not use this tool to start
27 |           * long-running processes (e.g. servers) that are not intended to terminate quickly,
28 |           * processes that require user interaction.
29 | 
30 |         :param command: the shell command to execute
31 |         :param cwd: the working directory to execute the command in. If None, the project root will be used.
32 |         :param capture_stderr: whether to capture and return stderr output
33 |         :param max_answer_chars: if the output is longer than this number of characters,
34 |             no content will be returned. -1 means using the default value, don't adjust unless there is no other way to get the content
35 |             required for the task.
36 |         :return: a JSON object containing the command's stdout and optionally stderr output
37 |         """
38 |         if cwd is None:
39 |             _cwd = self.get_project_root()
40 |         else:
41 |             if os.path.isabs(cwd):
42 |                 _cwd = cwd
43 |             else:
44 |                 _cwd = os.path.join(self.get_project_root(), cwd)
45 |                 if not os.path.isdir(_cwd):
46 |                     raise FileNotFoundError(
47 |                         f"Specified a relative working directory ({cwd}), but the resulting path is not a directory: {_cwd}"
48 |                     )
49 | 
50 |         result = execute_shell_command(command, cwd=_cwd, capture_stderr=capture_stderr)
51 |         result = result.json()
52 |         return self._limit_length(result, max_answer_chars)
53 | 
```

--------------------------------------------------------------------------------
/src/serena/util/thread.py:
--------------------------------------------------------------------------------

```python
 1 | import threading
 2 | from collections.abc import Callable
 3 | from enum import Enum
 4 | from typing import Generic, TypeVar
 5 | 
 6 | from sensai.util.string import ToStringMixin
 7 | 
 8 | 
 9 | class TimeoutException(Exception):
10 |     def __init__(self, message: str, timeout: float) -> None:
11 |         super().__init__(message)
12 |         self.timeout = timeout
13 | 
14 | 
15 | T = TypeVar("T")
16 | 
17 | 
18 | class ExecutionResult(Generic[T], ToStringMixin):
19 | 
20 |     class Status(Enum):
21 |         SUCCESS = "success"
22 |         TIMEOUT = "timeout"
23 |         EXCEPTION = "error"
24 | 
25 |     def __init__(self) -> None:
26 |         self.result_value: T | None = None
27 |         self.status: ExecutionResult.Status | None = None
28 |         self.exception: Exception | None = None
29 | 
30 |     def set_result_value(self, value: T) -> None:
31 |         self.result_value = value
32 |         self.status = ExecutionResult.Status.SUCCESS
33 | 
34 |     def set_timed_out(self, exception: TimeoutException) -> None:
35 |         self.exception = exception
36 |         self.status = ExecutionResult.Status.TIMEOUT
37 | 
38 |     def set_exception(self, exception: Exception) -> None:
39 |         self.exception = exception
40 |         self.status = ExecutionResult.Status.EXCEPTION
41 | 
42 | 
43 | def execute_with_timeout(func: Callable[[], T], timeout: float, function_name: str) -> ExecutionResult[T]:
44 |     """
45 |     Executes the given function with a timeout
46 | 
47 |     :param func: the function to execute
48 |     :param timeout: the timeout in seconds
49 |     :param function_name: the name of the function (for error messages)
50 |     :returns: the execution result
51 |     """
52 |     execution_result: ExecutionResult[T] = ExecutionResult()
53 | 
54 |     def target() -> None:
55 |         try:
56 |             value = func()
57 |             execution_result.set_result_value(value)
58 |         except Exception as e:
59 |             execution_result.set_exception(e)
60 | 
61 |     thread = threading.Thread(target=target, daemon=True)
62 |     thread.start()
63 |     thread.join(timeout=timeout)
64 | 
65 |     if thread.is_alive():
66 |         timeout_exception = TimeoutException(f"Execution of '{function_name}' timed out after {timeout} seconds.", timeout)
67 |         execution_result.set_timed_out(timeout_exception)
68 | 
69 |     return execution_result
70 | 
```

--------------------------------------------------------------------------------
/src/serena/constants.py:
--------------------------------------------------------------------------------

```python
 1 | from pathlib import Path
 2 | 
 3 | _repo_root_path = Path(__file__).parent.parent.parent.resolve()
 4 | _serena_pkg_path = Path(__file__).parent.resolve()
 5 | 
 6 | SERENA_MANAGED_DIR_NAME = ".serena"
 7 | _serena_in_home_managed_dir = Path.home() / ".serena"
 8 | 
 9 | SERENA_MANAGED_DIR_IN_HOME = str(_serena_in_home_managed_dir)
10 | 
11 | # TODO: Path-related constants should be moved to SerenaPaths; don't add further constants here.
12 | REPO_ROOT = str(_repo_root_path)
13 | PROMPT_TEMPLATES_DIR_INTERNAL = str(_serena_pkg_path / "resources" / "config" / "prompt_templates")
14 | PROMPT_TEMPLATES_DIR_IN_USER_HOME = str(_serena_in_home_managed_dir / "prompt_templates")
15 | SERENAS_OWN_CONTEXT_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "contexts")
16 | """The contexts that are shipped with the Serena package, i.e. the default contexts."""
17 | USER_CONTEXT_YAMLS_DIR = str(_serena_in_home_managed_dir / "contexts")
18 | """Contexts defined by the user. If a name of a context matches a name of a context in SERENAS_OWN_CONTEXT_YAMLS_DIR, the user context will override the default one."""
19 | SERENAS_OWN_MODE_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "modes")
20 | """The modes that are shipped with the Serena package, i.e. the default modes."""
21 | USER_MODE_YAMLS_DIR = str(_serena_in_home_managed_dir / "modes")
22 | """Modes defined by the user. If a name of a mode matches a name of a mode in SERENAS_OWN_MODE_YAMLS_DIR, the user mode will override the default one."""
23 | INTERNAL_MODE_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "internal_modes")
24 | """Internal modes, never overridden by user modes."""
25 | SERENA_DASHBOARD_DIR = str(_serena_pkg_path / "resources" / "dashboard")
26 | SERENA_ICON_DIR = str(_serena_pkg_path / "resources" / "icons")
27 | 
28 | DEFAULT_ENCODING = "utf-8"
29 | DEFAULT_CONTEXT = "desktop-app"
30 | DEFAULT_MODES = ("interactive", "editing")
31 | 
32 | PROJECT_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "project.template.yml")
33 | SERENA_CONFIG_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "serena_config.template.yml")
34 | 
35 | SERENA_LOG_FORMAT = "%(levelname)-5s %(asctime)-15s [%(threadName)s] %(name)s:%(funcName)s:%(lineno)d - %(message)s"
36 | 
```

--------------------------------------------------------------------------------
/src/serena/util/logging.py:
--------------------------------------------------------------------------------

```python
 1 | import queue
 2 | import threading
 3 | from collections.abc import Callable
 4 | 
 5 | from sensai.util import logging
 6 | 
 7 | from serena.constants import SERENA_LOG_FORMAT
 8 | 
 9 | 
10 | class MemoryLogHandler(logging.Handler):
11 |     def __init__(self, level: int = logging.NOTSET) -> None:
12 |         super().__init__(level=level)
13 |         self.setFormatter(logging.Formatter(SERENA_LOG_FORMAT))
14 |         self._log_buffer = LogBuffer()
15 |         self._log_queue: queue.Queue[str] = queue.Queue()
16 |         self._stop_event = threading.Event()
17 |         self._emit_callbacks: list[Callable[[str], None]] = []
18 | 
19 |         # start background thread to process logs
20 |         self.worker_thread = threading.Thread(target=self._process_queue, daemon=True)
21 |         self.worker_thread.start()
22 | 
23 |     def add_emit_callback(self, callback: Callable[[str], None]) -> None:
24 |         """
25 |         Adds a callback that will be called with each log message.
26 |         The callback should accept a single string argument (the log message).
27 |         """
28 |         self._emit_callbacks.append(callback)
29 | 
30 |     def emit(self, record: logging.LogRecord) -> None:
31 |         msg = self.format(record)
32 |         self._log_queue.put_nowait(msg)
33 | 
34 |     def _process_queue(self) -> None:
35 |         while not self._stop_event.is_set():
36 |             try:
37 |                 msg = self._log_queue.get(timeout=1)
38 |                 self._log_buffer.append(msg)
39 |                 for callback in self._emit_callbacks:
40 |                     try:
41 |                         callback(msg)
42 |                     except:
43 |                         pass
44 |                 self._log_queue.task_done()
45 |             except queue.Empty:
46 |                 continue
47 | 
48 |     def get_log_messages(self) -> list[str]:
49 |         return self._log_buffer.get_log_messages()
50 | 
51 | 
52 | class LogBuffer:
53 |     """
54 |     A thread-safe buffer for storing log messages.
55 |     """
56 | 
57 |     def __init__(self) -> None:
58 |         self._log_messages: list[str] = []
59 |         self._lock = threading.Lock()
60 | 
61 |     def append(self, msg: str) -> None:
62 |         with self._lock:
63 |             self._log_messages.append(msg)
64 | 
65 |     def get_log_messages(self) -> list[str]:
66 |         with self._lock:
67 |             return self._log_messages.copy()
68 | 
```

--------------------------------------------------------------------------------
/test/resources/repos/bash/test_repo/config.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | # Configuration script for project setup
 4 | 
 5 | # Environment variables
 6 | export PROJECT_NAME="bash-test-project"
 7 | export PROJECT_VERSION="1.0.0"
 8 | export LOG_LEVEL="INFO"
 9 | export CONFIG_DIR="./config"
10 | 
11 | # Default settings
12 | DEFAULT_TIMEOUT=30
13 | DEFAULT_RETRIES=3
14 | DEFAULT_PORT=8080
15 | 
16 | # Configuration arrays
17 | declare -A ENVIRONMENTS=(
18 |     ["dev"]="development"
19 |     ["prod"]="production"
20 |     ["test"]="testing"
21 | )
22 | 
23 | declare -A DATABASE_CONFIGS=(
24 |     ["host"]="localhost"
25 |     ["port"]="5432"
26 |     ["name"]="myapp_db"
27 |     ["user"]="dbuser"
28 | )
29 | 
30 | # Function to load configuration
31 | load_config() {
32 |     local env="${1:-dev}"
33 |     local config_file="${CONFIG_DIR}/${env}.conf"
34 |     
35 |     if [[ -f "$config_file" ]]; then
36 |         echo "Loading configuration from $config_file"
37 |         source "$config_file"
38 |     else
39 |         echo "Warning: Configuration file $config_file not found, using defaults"
40 |     fi
41 | }
42 | 
43 | # Function to validate configuration
44 | validate_config() {
45 |     local errors=0
46 |     
47 |     if [[ -z "$PROJECT_NAME" ]]; then
48 |         echo "Error: PROJECT_NAME is not set" >&2
49 |         ((errors++))
50 |     fi
51 |     
52 |     if [[ -z "$PROJECT_VERSION" ]]; then
53 |         echo "Error: PROJECT_VERSION is not set" >&2
54 |         ((errors++))
55 |     fi
56 |     
57 |     if [[ $DEFAULT_PORT -lt 1024 || $DEFAULT_PORT -gt 65535 ]]; then
58 |         echo "Error: Invalid port number $DEFAULT_PORT" >&2
59 |         ((errors++))
60 |     fi
61 |     
62 |     return $errors
63 | }
64 | 
65 | # Function to print configuration
66 | print_config() {
67 |     echo "=== Current Configuration ==="
68 |     echo "Project Name: $PROJECT_NAME"
69 |     echo "Version: $PROJECT_VERSION"
70 |     echo "Log Level: $LOG_LEVEL"
71 |     echo "Default Port: $DEFAULT_PORT"
72 |     echo "Default Timeout: $DEFAULT_TIMEOUT"
73 |     echo "Default Retries: $DEFAULT_RETRIES"
74 |     
75 |     echo "\n=== Environments ==="
76 |     for env in "${!ENVIRONMENTS[@]}"; do
77 |         echo "  $env: ${ENVIRONMENTS[$env]}"
78 |     done
79 |     
80 |     echo "\n=== Database Configuration ==="
81 |     for key in "${!DATABASE_CONFIGS[@]}"; do
82 |         echo "  $key: ${DATABASE_CONFIGS[$key]}"
83 |     done
84 | }
85 | 
86 | # Initialize configuration if this script is run directly
87 | if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
88 |     load_config "$1"
89 |     validate_config
90 |     print_config
91 | fi
92 | 
```

--------------------------------------------------------------------------------
/src/serena/util/exception.py:
--------------------------------------------------------------------------------

```python
 1 | import os
 2 | import sys
 3 | 
 4 | from serena.agent import log
 5 | 
 6 | 
 7 | def is_headless_environment() -> bool:
 8 |     """
 9 |     Detect if we're running in a headless environment where GUI operations would fail.
10 | 
11 |     Returns True if:
12 |     - No DISPLAY variable on Linux/Unix
13 |     - Running in SSH session
14 |     - Running in WSL without X server
15 |     - Running in Docker container
16 |     """
17 |     # Check if we're on Windows - GUI usually works there
18 |     if sys.platform == "win32":
19 |         return False
20 | 
21 |     # Check for DISPLAY variable (required for X11)
22 |     if not os.environ.get("DISPLAY"):  # type: ignore
23 |         return True
24 | 
25 |     # Check for SSH session
26 |     if os.environ.get("SSH_CONNECTION") or os.environ.get("SSH_CLIENT"):
27 |         return True
28 | 
29 |     # Check for common CI/container environments
30 |     if os.environ.get("CI") or os.environ.get("CONTAINER") or os.path.exists("/.dockerenv"):
31 |         return True
32 | 
33 |     # Check for WSL (only on Unix-like systems where os.uname exists)
34 |     if hasattr(os, "uname"):
35 |         if "microsoft" in os.uname().release.lower():
36 |             # In WSL, even with DISPLAY set, X server might not be running
37 |             # This is a simplified check - could be improved
38 |             return True
39 | 
40 |     return False
41 | 
42 | 
43 | def show_fatal_exception_safe(e: Exception) -> None:
44 |     """
45 |     Shows the given exception in the GUI log viewer on the main thread and ensures that the exception is logged or at
46 |     least printed to stderr.
47 |     """
48 |     # Log the error and print it to stderr
49 |     log.error(f"Fatal exception: {e}", exc_info=e)
50 |     print(f"Fatal exception: {e}", file=sys.stderr)
51 | 
52 |     # Don't attempt GUI in headless environments
53 |     if is_headless_environment():
54 |         log.debug("Skipping GUI error display in headless environment")
55 |         return
56 | 
57 |     # attempt to show the error in the GUI
58 |     try:
59 |         # NOTE: The import can fail on macOS if Tk is not available (depends on Python interpreter installation, which uv
60 |         #   used as a base); while tkinter as such is always available, its dependencies can be unavailable on macOS.
61 |         from serena.gui_log_viewer import show_fatal_exception
62 | 
63 |         show_fatal_exception(e)
64 |     except Exception as gui_error:
65 |         log.debug(f"Failed to show GUI error dialog: {gui_error}")
66 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/ruby/test_ruby_basic.py:
--------------------------------------------------------------------------------

```python
 1 | import os
 2 | from pathlib import Path
 3 | 
 4 | import pytest
 5 | 
 6 | from solidlsp import SolidLanguageServer
 7 | from solidlsp.ls_config import Language
 8 | from solidlsp.ls_utils import SymbolUtils
 9 | 
10 | 
11 | @pytest.mark.ruby
12 | class TestRubyLanguageServer:
13 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
14 |     def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
15 |         symbols = language_server.request_full_symbol_tree()
16 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "DemoClass"), "DemoClass not found in symbol tree"
17 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "helper_function"), "helper_function not found in symbol tree"
18 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "print_value"), "print_value not found in symbol tree"
19 | 
20 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
21 |     def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
22 |         file_path = os.path.join("main.rb")
23 |         symbols = language_server.request_document_symbols(file_path)
24 |         helper_symbol = None
25 |         for sym in symbols[0]:
26 |             if sym.get("name") == "helper_function":
27 |                 helper_symbol = sym
28 |                 break
29 |         print(helper_symbol)
30 |         assert helper_symbol is not None, "Could not find 'helper_function' symbol in main.rb"
31 | 
32 |     @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True)
33 |     @pytest.mark.parametrize("repo_path", [Language.RUBY], indirect=True)
34 |     def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
35 |         # Test finding Calculator.add method definition from line 17: Calculator.new.add(demo.value, 10)
36 |         definition_location_list = language_server.request_definition(
37 |             str(repo_path / "main.rb"), 16, 17
38 |         )  # add method at line 17 (0-indexed 16), position 17
39 | 
40 |         assert len(definition_location_list) == 1
41 |         definition_location = definition_location_list[0]
42 |         print(f"Found definition: {definition_location}")
43 |         assert definition_location["uri"].endswith("lib.rb")
44 |         assert definition_location["range"]["start"]["line"] == 1  # add method on line 2 (0-indexed 1)
45 | 
```

--------------------------------------------------------------------------------
/test/resources/repos/python/test_repo/test_repo/services.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Services module demonstrating function usage and dependencies.
 3 | """
 4 | 
 5 | from typing import Any
 6 | 
 7 | from .models import Item, User
 8 | 
 9 | 
10 | class UserService:
11 |     """Service for user-related operations"""
12 | 
13 |     def __init__(self, user_db: dict[str, User] | None = None):
14 |         self.users = user_db or {}
15 | 
16 |     def create_user(self, id: str, name: str, email: str) -> User:
17 |         """Create a new user and store it"""
18 |         if id in self.users:
19 |             raise ValueError(f"User with ID {id} already exists")
20 | 
21 |         user = User(id=id, name=name, email=email)
22 |         self.users[id] = user
23 |         return user
24 | 
25 |     def get_user(self, id: str) -> User | None:
26 |         """Get a user by ID"""
27 |         return self.users.get(id)
28 | 
29 |     def list_users(self) -> list[User]:
30 |         """Get a list of all users"""
31 |         return list(self.users.values())
32 | 
33 |     def delete_user(self, id: str) -> bool:
34 |         """Delete a user by ID"""
35 |         if id in self.users:
36 |             del self.users[id]
37 |             return True
38 |         return False
39 | 
40 | 
41 | class ItemService:
42 |     """Service for item-related operations"""
43 | 
44 |     def __init__(self, item_db: dict[str, Item] | None = None):
45 |         self.items = item_db or {}
46 | 
47 |     def create_item(self, id: str, name: str, price: float, category: str) -> Item:
48 |         """Create a new item and store it"""
49 |         if id in self.items:
50 |             raise ValueError(f"Item with ID {id} already exists")
51 | 
52 |         item = Item(id=id, name=name, price=price, category=category)
53 |         self.items[id] = item
54 |         return item
55 | 
56 |     def get_item(self, id: str) -> Item | None:
57 |         """Get an item by ID"""
58 |         return self.items.get(id)
59 | 
60 |     def list_items(self, category: str | None = None) -> list[Item]:
61 |         """List all items, optionally filtered by category"""
62 |         if category:
63 |             return [item for item in self.items.values() if item.category == category]
64 |         return list(self.items.values())
65 | 
66 | 
67 | # Factory function for services
68 | def create_service_container() -> dict[str, Any]:
69 |     """Create a container with all services"""
70 |     container = {"user_service": UserService(), "item_service": ItemService()}
71 |     return container
72 | 
73 | 
74 | user_var_str = "user_var"
75 | 
76 | 
77 | user_service = UserService()
78 | user_service.create_user("1", "Alice", "[email protected]")
79 | 
```

--------------------------------------------------------------------------------
/src/serena/tools/memory_tools.py:
--------------------------------------------------------------------------------

```python
 1 | import json
 2 | 
 3 | from serena.tools import Tool
 4 | 
 5 | 
 6 | class WriteMemoryTool(Tool):
 7 |     """
 8 |     Writes a named memory (for future reference) to Serena's project-specific memory store.
 9 |     """
10 | 
11 |     def apply(self, memory_name: str, content: str, max_answer_chars: int = -1) -> str:
12 |         """
13 |         Write some information about this project that can be useful for future tasks to a memory in md format.
14 |         The memory name should be meaningful.
15 |         """
16 |         if max_answer_chars == -1:
17 |             max_answer_chars = self.agent.serena_config.default_max_tool_answer_chars
18 |         if len(content) > max_answer_chars:
19 |             raise ValueError(
20 |                 f"Content for {memory_name} is too long. Max length is {max_answer_chars} characters. " + "Please make the content shorter."
21 |             )
22 | 
23 |         return self.memories_manager.save_memory(memory_name, content)
24 | 
25 | 
26 | class ReadMemoryTool(Tool):
27 |     """
28 |     Reads the memory with the given name from Serena's project-specific memory store.
29 |     """
30 | 
31 |     def apply(self, memory_file_name: str, max_answer_chars: int = -1) -> str:
32 |         """
33 |         Read the content of a memory file. This tool should only be used if the information
34 |         is relevant to the current task. You can infer whether the information
35 |         is relevant from the memory file name.
36 |         You should not read the same memory file multiple times in the same conversation.
37 |         """
38 |         return self.memories_manager.load_memory(memory_file_name)
39 | 
40 | 
41 | class ListMemoriesTool(Tool):
42 |     """
43 |     Lists memories in Serena's project-specific memory store.
44 |     """
45 | 
46 |     def apply(self) -> str:
47 |         """
48 |         List available memories. Any memory can be read using the `read_memory` tool.
49 |         """
50 |         return json.dumps(self.memories_manager.list_memories())
51 | 
52 | 
53 | class DeleteMemoryTool(Tool):
54 |     """
55 |     Deletes a memory from Serena's project-specific memory store.
56 |     """
57 | 
58 |     def apply(self, memory_file_name: str) -> str:
59 |         """
60 |         Delete a memory file. Should only happen if a user asks for it explicitly,
61 |         for example by saying that the information retrieved from a memory file is no longer correct
62 |         or no longer relevant for the project.
63 |         """
64 |         return self.memories_manager.delete_memory(memory_file_name)
65 | 
```

--------------------------------------------------------------------------------
/test/resources/repos/python/test_repo/test_repo/overloaded.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Module demonstrating function and method overloading with typing.overload
 3 | """
 4 | 
 5 | from typing import Any, overload
 6 | 
 7 | 
 8 | # Example of function overloading
 9 | @overload
10 | def process_data(data: str) -> dict[str, str]: ...
11 | 
12 | 
13 | @overload
14 | def process_data(data: int) -> dict[str, int]: ...
15 | 
16 | 
17 | @overload
18 | def process_data(data: list[str | int]) -> dict[str, list[str | int]]: ...
19 | 
20 | 
21 | def process_data(data: str | int | list[str | int]) -> dict[str, Any]:
22 |     """
23 |     Process data based on its type.
24 | 
25 |     - If string: returns a dict with 'value': <string>
26 |     - If int: returns a dict with 'value': <int>
27 |     - If list: returns a dict with 'value': <list>
28 |     """
29 |     return {"value": data}
30 | 
31 | 
32 | # Class with overloaded methods
33 | class DataProcessor:
34 |     """
35 |     A class demonstrating method overloading.
36 |     """
37 | 
38 |     @overload
39 |     def transform(self, input_value: str) -> str: ...
40 | 
41 |     @overload
42 |     def transform(self, input_value: int) -> int: ...
43 | 
44 |     @overload
45 |     def transform(self, input_value: list[Any]) -> list[Any]: ...
46 | 
47 |     def transform(self, input_value: str | int | list[Any]) -> str | int | list[Any]:
48 |         """
49 |         Transform input based on its type.
50 | 
51 |         - If string: returns the string in uppercase
52 |         - If int: returns the int multiplied by 2
53 |         - If list: returns the list sorted
54 |         """
55 |         if isinstance(input_value, str):
56 |             return input_value.upper()
57 |         elif isinstance(input_value, int):
58 |             return input_value * 2
59 |         elif isinstance(input_value, list):
60 |             try:
61 |                 return sorted(input_value)
62 |             except TypeError:
63 |                 return input_value
64 |         return input_value
65 | 
66 |     @overload
67 |     def fetch(self, id: int) -> dict[str, Any]: ...
68 | 
69 |     @overload
70 |     def fetch(self, id: str, cache: bool = False) -> dict[str, Any] | None: ...
71 | 
72 |     def fetch(self, id: int | str, cache: bool = False) -> dict[str, Any] | None:
73 |         """
74 |         Fetch data for a given ID.
75 | 
76 |         Args:
77 |             id: The ID to fetch, either numeric or string
78 |             cache: Whether to use cache for string IDs
79 | 
80 |         Returns:
81 |             Data dictionary or None if not found
82 | 
83 |         """
84 |         # Implementation would actually fetch data
85 |         if isinstance(id, int):
86 |             return {"id": id, "type": "numeric"}
87 |         else:
88 |             return {"id": id, "type": "string", "cached": cache}
89 | 
```

--------------------------------------------------------------------------------
/test/resources/repos/ruby/test_repo/examples/user_management.rb:
--------------------------------------------------------------------------------

```ruby
  1 | require '../services.rb'
  2 | require '../models.rb'
  3 | 
  4 | class UserStats
  5 |   attr_reader :user_count, :active_users, :last_updated
  6 | 
  7 |   def initialize
  8 |     @user_count = 0
  9 |     @active_users = 0
 10 |     @last_updated = Time.now
 11 |   end
 12 | 
 13 |   def update_stats(total, active)
 14 |     @user_count = total
 15 |     @active_users = active
 16 |     @last_updated = Time.now
 17 |   end
 18 | 
 19 |   def activity_ratio
 20 |     return 0.0 if @user_count == 0
 21 |     (@active_users.to_f / @user_count * 100).round(2)
 22 |   end
 23 | 
 24 |   def formatted_stats
 25 |     "Users: #{@user_count}, Active: #{@active_users} (#{activity_ratio}%)"
 26 |   end
 27 | end
 28 | 
 29 | class UserManager
 30 |   def initialize
 31 |     @service = Services::UserService.new
 32 |     @stats = UserStats.new
 33 |   end
 34 | 
 35 |   def create_user_with_tracking(id, name, email = nil)
 36 |     user = @service.create_user(id, name)
 37 |     user.email = email if email
 38 |     
 39 |     update_statistics
 40 |     notify_user_created(user)
 41 |     
 42 |     user
 43 |   end
 44 | 
 45 |   def get_user_details(id)
 46 |     user = @service.get_user(id)
 47 |     return nil unless user
 48 |     
 49 |     {
 50 |       user_info: user.full_info,
 51 |       created_at: Time.now,
 52 |       stats: @stats.formatted_stats
 53 |     }
 54 |   end
 55 | 
 56 |   def bulk_create_users(user_data_list)
 57 |     created_users = []
 58 |     
 59 |     user_data_list.each do |data|
 60 |       user = create_user_with_tracking(data[:id], data[:name], data[:email])
 61 |       created_users << user
 62 |     end
 63 |     
 64 |     created_users
 65 |   end
 66 | 
 67 |   private
 68 | 
 69 |   def update_statistics
 70 |     total_users = @service.users.length
 71 |     # For demo purposes, assume all users are active
 72 |     @stats.update_stats(total_users, total_users)
 73 |   end
 74 | 
 75 |   def notify_user_created(user)
 76 |     puts "User created: #{user.name} (ID: #{user.id})"
 77 |   end
 78 | end
 79 | 
 80 | def process_user_data(raw_data)
 81 |   processed = raw_data.map do |entry|
 82 |     {
 83 |       id: entry["id"] || entry[:id],
 84 |       name: entry["name"] || entry[:name],
 85 |       email: entry["email"] || entry[:email]
 86 |     }
 87 |   end
 88 |   
 89 |   processed.reject { |entry| entry[:name].nil? || entry[:name].empty? }
 90 | end
 91 | 
 92 | def main
 93 |   # Example usage
 94 |   manager = UserManager.new
 95 |   
 96 |   sample_data = [
 97 |     { id: 1, name: "Alice Johnson", email: "[email protected]" },
 98 |     { id: 2, name: "Bob Smith", email: "[email protected]" },
 99 |     { id: 3, name: "Charlie Brown" }
100 |   ]
101 |   
102 |   users = manager.bulk_create_users(sample_data)
103 |   
104 |   users.each do |user|
105 |     details = manager.get_user_details(user.id)
106 |     puts details[:user_info]
107 |   end
108 |   
109 |   puts "\nFinal statistics:"
110 |   stats = UserStats.new
111 |   stats.update_stats(users.length, users.length)
112 |   puts stats.formatted_stats
113 | end
114 | 
115 | # Execute if this file is run directly
116 | main if __FILE__ == $0
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Base stage with common dependencies
 2 | FROM python:3.11-slim AS base
 3 | SHELL ["/bin/bash", "-c"]
 4 | 
 5 | # Set environment variables to make Python print directly to the terminal and avoid .pyc files.
 6 | ENV PYTHONUNBUFFERED=1
 7 | ENV PYTHONDONTWRITEBYTECODE=1
 8 | 
 9 | # Install system dependencies required for package manager and build tools.
10 | # sudo, wget, zip needed for some assistants, like junie
11 | RUN apt-get update && apt-get install -y --no-install-recommends \
12 |     curl \
13 |     build-essential \
14 |     git \
15 |     ssh \
16 |     sudo \
17 |     wget \
18 |     zip \
19 |     unzip \
20 |     git \
21 |     && rm -rf /var/lib/apt/lists/*
22 | 
23 | # Install pipx.
24 | RUN python3 -m pip install --no-cache-dir pipx \
25 |     && pipx ensurepath
26 | 
27 | # Install nodejs
28 | ENV NVM_VERSION=0.40.3
29 | ENV NODE_VERSION=22.18.0
30 | RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh | bash
31 | # standard location
32 | ENV NVM_DIR=/root/.nvm
33 | RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION}
34 | RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION}
35 | RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION}
36 | ENV PATH="${NVM_DIR}/versions/node/v${NODE_VERSION}/bin/:${PATH}"
37 | 
38 | # Add local bin to the path
39 | ENV PATH="${PATH}:/root/.local/bin"
40 | 
41 | # Install the latest version of uv
42 | RUN curl -LsSf https://astral.sh/uv/install.sh | sh
43 | 
44 | # Install Rust and rustup for rust-analyzer support (minimal profile)
45 | ENV RUSTUP_HOME=/usr/local/rustup
46 | ENV CARGO_HOME=/usr/local/cargo
47 | ENV PATH="${CARGO_HOME}/bin:${PATH}"
48 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
49 |     --default-toolchain stable \
50 |     --profile minimal \
51 |     && rustup component add rust-analyzer
52 | 
53 | # Set the working directory
54 | WORKDIR /workspaces/serena
55 | 
56 | # Development target
57 | FROM base AS development
58 | # Copy all files for development
59 | COPY . /workspaces/serena/
60 | 
61 | # Create virtual environment and install dependencies with dev extras
62 | RUN uv venv
63 | RUN . .venv/bin/activate
64 | RUN uv pip install --all-extras -r pyproject.toml -e .
65 | ENV PATH="/workspaces/serena/.venv/bin:${PATH}"
66 | 
67 | # Entrypoint to ensure environment is activated
68 | ENTRYPOINT ["/bin/bash", "-c", "source .venv/bin/activate && $0 $@"]
69 | 
70 | # Production target
71 | FROM base AS production
72 | # Copy only necessary files for production
73 | COPY pyproject.toml /workspaces/serena/
74 | COPY README.md /workspaces/serena/
75 | COPY src/ /workspaces/serena/src/
76 | 
77 | # Create virtual environment and install dependencies (production only)
78 | RUN uv venv
79 | RUN . .venv/bin/activate
80 | RUN uv pip install -r pyproject.toml -e .
81 | ENV PATH="/workspaces/serena/.venv/bin:${PATH}"
82 | 
83 | # Entrypoint to ensure environment is activated
84 | ENTRYPOINT ["/bin/bash", "-c", "source .venv/bin/activate && $0 $@"]
85 | 
86 | 
```

--------------------------------------------------------------------------------
/docs/custom_agent.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Custom Agents with Serena
 2 | 
 3 | As a reference implementation, we provide an integration with the [Agno](https://docs.agno.com/introduction/playground) agent framework.
 4 | Agno is a model-agnostic agent framework that allows you to turn Serena into an agent 
 5 | (independent of the MCP technology) with a large number of underlying LLMs. While Agno has recently
 6 | added support for MCP servers out of the box, our Agno integration predates this and is a good illustration of how
 7 | easy it is to integrate Serena into an arbitrary agent framework.
 8 | 
 9 | Here's how it works:
10 | 
11 | 1. Download the agent-ui code with npx
12 |    ```shell
13 |    npx create-agent-ui@latest
14 |    ```
15 |    or, alternatively, clone it manually:
16 |    ```shell
17 |    git clone https://github.com/agno-agi/agent-ui.git
18 |    cd agent-ui 
19 |    pnpm install 
20 |    pnpm dev
21 |    ```
22 | 
23 | 2. Install serena with the optional requirements:
24 |    ```shell
25 |    # You can also only select agno,google or agno,anthropic instead of all-extras
26 |    uv pip install --all-extras -r pyproject.toml -e .
27 |    ```
28 |    
29 | 3. Copy `.env.example` to `.env` and fill in the API keys for the provider(s) you
30 |    intend to use.
31 | 
32 | 4. Start the agno agent app with
33 |    ```shell
34 |    uv run python scripts/agno_agent.py
35 |    ```
36 |    By default, the script uses Claude as the model, but you can choose any model
37 |    supported by Agno (which is essentially any existing model).
38 | 
39 | 5. In a new terminal, start the agno UI with
40 |    ```shell
41 |    cd agent-ui 
42 |    pnpm dev
43 |    ```
44 |    Connect the UI to the agent you started above and start chatting. You will have
45 |    the same tools as in the MCP server version.
46 | 
47 | 
48 | Here is a short demo of Serena performing a small analysis task with the newest Gemini model:
49 | 
50 | https://github.com/user-attachments/assets/ccfcb968-277d-4ca9-af7f-b84578858c62
51 | 
52 | 
53 | ⚠️ IMPORTANT: In contrast to the MCP server approach, tool execution in the Agno UI does
54 | not ask for the user's permission. The shell tool is particularly critical, as it can perform arbitrary code execution. 
55 | While we have never encountered any issues with
56 | this in our testing with Claude, allowing this may not be entirely safe. 
57 | You may choose to disable certain tools for your setup in your Serena project's
58 | configuration file (`.yml`).
59 | 
60 | 
61 | ## Other Agent Frameworks
62 | 
63 | It should be straightforward to incorporate Serena into any
64 | agent framework (like [pydantic-ai](https://ai.pydantic.dev/), [langgraph](https://langchain-ai.github.io/langgraph/tutorials/introduction/) or others).
65 | Typically, you need only to write an adapter for Serena's tools to the tool representation in the framework of your choice, 
66 | as was done by us for Agno with [SerenaAgnoToolkit](/src/serena/agno.py).
67 | 
68 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/python/test_retrieval_with_ignored_dirs.py:
--------------------------------------------------------------------------------

```python
 1 | from collections.abc import Generator
 2 | from pathlib import Path
 3 | 
 4 | import pytest
 5 | 
 6 | from solidlsp import SolidLanguageServer
 7 | from solidlsp.ls_config import Language
 8 | from test.conftest import create_ls
 9 | 
10 | # This mark will be applied to all tests in this module
11 | pytestmark = pytest.mark.python
12 | 
13 | 
14 | @pytest.fixture(scope="module")
15 | def ls_with_ignored_dirs() -> Generator[SolidLanguageServer, None, None]:
16 |     """Fixture to set up an LS for the python test repo with the 'scripts' directory ignored."""
17 |     ignored_paths = ["scripts", "custom_test"]
18 |     ls = create_ls(ignored_paths=ignored_paths, language=Language.PYTHON)
19 |     ls.start()
20 |     try:
21 |         yield ls
22 |     finally:
23 |         ls.stop()
24 | 
25 | 
26 | @pytest.mark.parametrize("ls_with_ignored_dirs", [Language.PYTHON], indirect=True)
27 | def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
28 |     """Tests that request_full_symbol_tree ignores the configured directory."""
29 |     root = ls_with_ignored_dirs.request_full_symbol_tree()[0]
30 |     root_children = root["children"]
31 |     children_names = {child["name"] for child in root_children}
32 |     assert children_names == {"test_repo", "examples"}
33 | 
34 | 
35 | @pytest.mark.parametrize("ls_with_ignored_dirs", [Language.PYTHON], indirect=True)
36 | def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
37 |     """Tests that find_references ignores the configured directory."""
38 |     # Location of Item, which is referenced in scripts
39 |     definition_file = "test_repo/models.py"
40 |     definition_line = 56
41 |     definition_col = 6
42 | 
43 |     references = ls_with_ignored_dirs.request_references(definition_file, definition_line, definition_col)
44 | 
45 |     # assert that scripts does not appear in the references
46 |     assert not any("scripts" in ref["relativePath"] for ref in references)
47 | 
48 | 
49 | @pytest.mark.parametrize("repo_path", [Language.PYTHON], indirect=True)
50 | def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
51 |     """Tests that refs and symbols with glob patterns are ignored."""
52 |     ignored_paths = ["*ipts", "custom_t*"]
53 |     ls = create_ls(ignored_paths=ignored_paths, repo_path=str(repo_path), language=Language.PYTHON)
54 |     ls.start()
55 |     # same as in the above tests
56 |     root = ls.request_full_symbol_tree()[0]
57 |     root_children = root["children"]
58 |     children_names = {child["name"] for child in root_children}
59 |     assert children_names == {"test_repo", "examples"}
60 | 
61 |     # test that the refs and symbols with glob patterns are ignored
62 |     definition_file = "test_repo/models.py"
63 |     definition_line = 56
64 |     definition_col = 6
65 | 
66 |     references = ls.request_references(definition_file, definition_line, definition_col)
67 |     assert not any("scripts" in ref["relativePath"] for ref in references)
68 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/java/test_java_basic.py:
--------------------------------------------------------------------------------

```python
 1 | import os
 2 | 
 3 | import pytest
 4 | 
 5 | from solidlsp import SolidLanguageServer
 6 | from solidlsp.ls_config import Language
 7 | from solidlsp.ls_utils import SymbolUtils
 8 | 
 9 | 
10 | @pytest.mark.java
11 | class TestJavaLanguageServer:
12 |     @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True)
13 |     def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
14 |         symbols = language_server.request_full_symbol_tree()
15 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main class not found in symbol tree"
16 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils class not found in symbol tree"
17 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model class not found in symbol tree"
18 | 
19 |     @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True)
20 |     def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
21 |         # Use correct Maven/Java file paths
22 |         file_path = os.path.join("src", "main", "java", "test_repo", "Utils.java")
23 |         refs = language_server.request_references(file_path, 4, 20)
24 |         assert any("Main.java" in ref.get("relativePath", "") for ref in refs), "Main should reference Utils.printHello"
25 | 
26 |         # Dynamically determine the correct line/column for the 'Model' class name
27 |         file_path = os.path.join("src", "main", "java", "test_repo", "Model.java")
28 |         symbols = language_server.request_document_symbols(file_path)
29 |         model_symbol = None
30 |         for sym in symbols[0]:
31 |             if sym.get("name") == "Model" and sym.get("kind") == 5:  # 5 = Class
32 |                 model_symbol = sym
33 |                 break
34 |         assert model_symbol is not None, "Could not find 'Model' class symbol in Model.java"
35 |         # Use selectionRange if present, otherwise fall back to range
36 |         if "selectionRange" in model_symbol:
37 |             sel_start = model_symbol["selectionRange"]["start"]
38 |         else:
39 |             sel_start = model_symbol["range"]["start"]
40 |         refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
41 |         assert any(
42 |             "Main.java" in ref.get("relativePath", "") for ref in refs
43 |         ), "Main should reference Model (tried all positions in selectionRange)"
44 | 
45 |     @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True)
46 |     def test_overview_methods(self, language_server: SolidLanguageServer) -> None:
47 |         symbols = language_server.request_full_symbol_tree()
48 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main missing from overview"
49 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils missing from overview"
50 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model missing from overview"
51 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/kotlin/test_kotlin_basic.py:
--------------------------------------------------------------------------------

```python
 1 | import os
 2 | 
 3 | import pytest
 4 | 
 5 | from solidlsp import SolidLanguageServer
 6 | from solidlsp.ls_config import Language
 7 | from solidlsp.ls_utils import SymbolUtils
 8 | 
 9 | 
10 | @pytest.mark.kotlin
11 | class TestKotlinLanguageServer:
12 |     @pytest.mark.parametrize("language_server", [Language.KOTLIN], indirect=True)
13 |     def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
14 |         symbols = language_server.request_full_symbol_tree()
15 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main class not found in symbol tree"
16 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils class not found in symbol tree"
17 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model class not found in symbol tree"
18 | 
19 |     @pytest.mark.parametrize("language_server", [Language.KOTLIN], indirect=True)
20 |     def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
21 |         # Use correct Kotlin file paths
22 |         file_path = os.path.join("src", "main", "kotlin", "test_repo", "Utils.kt")
23 |         refs = language_server.request_references(file_path, 3, 12)
24 |         assert any("Main.kt" in ref.get("relativePath", "") for ref in refs), "Main should reference Utils.printHello"
25 | 
26 |         # Dynamically determine the correct line/column for the 'Model' class name
27 |         file_path = os.path.join("src", "main", "kotlin", "test_repo", "Model.kt")
28 |         symbols = language_server.request_document_symbols(file_path)
29 |         model_symbol = None
30 |         for sym in symbols[0]:
31 |             print(sym)
32 |             print("\n")
33 |             if sym.get("name") == "Model" and sym.get("kind") == 23:  # 23 = Class
34 |                 model_symbol = sym
35 |                 break
36 |         assert model_symbol is not None, "Could not find 'Model' class symbol in Model.kt"
37 |         # Use selectionRange if present, otherwise fall back to range
38 |         if "selectionRange" in model_symbol:
39 |             sel_start = model_symbol["selectionRange"]["start"]
40 |         else:
41 |             sel_start = model_symbol["range"]["start"]
42 |         refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
43 |         assert any(
44 |             "Main.kt" in ref.get("relativePath", "") for ref in refs
45 |         ), "Main should reference Model (tried all positions in selectionRange)"
46 | 
47 |     @pytest.mark.parametrize("language_server", [Language.KOTLIN], indirect=True)
48 |     def test_overview_methods(self, language_server: SolidLanguageServer) -> None:
49 |         symbols = language_server.request_full_symbol_tree()
50 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main missing from overview"
51 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils missing from overview"
52 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model missing from overview"
53 | 
```

--------------------------------------------------------------------------------
/test/resources/repos/python/test_repo/test_repo/variables.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Test module for variable declarations and usage.
 3 | 
 4 | This module tests various types of variable declarations and usages including:
 5 | - Module-level variables
 6 | - Class-level variables
 7 | - Instance variables
 8 | - Variable reassignments
 9 | """
10 | 
11 | from dataclasses import dataclass, field
12 | 
13 | # Module-level variables
14 | module_var = "Initial module value"
15 | 
16 | reassignable_module_var = 10
17 | reassignable_module_var = 20  # Reassigned
18 | 
19 | # Module-level variable with type annotation
20 | typed_module_var: int = 42
21 | 
22 | 
23 | # Regular class with class and instance variables
24 | class VariableContainer:
25 |     """Class that contains various variables."""
26 | 
27 |     # Class-level variables
28 |     class_var = "Initial class value"
29 | 
30 |     reassignable_class_var = True
31 |     reassignable_class_var = False  # Reassigned #noqa: PIE794
32 | 
33 |     # Class-level variable with type annotation
34 |     typed_class_var: str = "typed value"
35 | 
36 |     def __init__(self):
37 |         # Instance variables
38 |         self.instance_var = "Initial instance value"
39 |         self.reassignable_instance_var = 100
40 | 
41 |         # Instance variable with type annotation
42 |         self.typed_instance_var: list[str] = ["item1", "item2"]
43 | 
44 |     def modify_instance_var(self):
45 |         # Reassign instance variable
46 |         self.instance_var = "Modified instance value"
47 |         self.reassignable_instance_var = 200  # Reassigned
48 | 
49 |     def use_module_var(self):
50 |         # Use module-level variables
51 |         result = module_var + " used in method"
52 |         other_result = reassignable_module_var + 5
53 |         return result, other_result
54 | 
55 |     def use_class_var(self):
56 |         # Use class-level variables
57 |         result = VariableContainer.class_var + " used in method"
58 |         other_result = VariableContainer.reassignable_class_var
59 |         return result, other_result
60 | 
61 | 
62 | # Dataclass with variables
63 | @dataclass
64 | class VariableDataclass:
65 |     """Dataclass that contains various fields."""
66 | 
67 |     # Field variables with type annotations
68 |     id: int
69 |     name: str
70 |     items: list[str] = field(default_factory=list)
71 |     metadata: dict[str, str] = field(default_factory=dict)
72 |     optional_value: float | None = None
73 | 
74 |     # This will be reassigned in various places
75 |     status: str = "pending"
76 | 
77 | 
78 | # Function that uses the module variables
79 | def use_module_variables():
80 |     """Function that uses module-level variables."""
81 |     result = module_var + " used in function"
82 |     other_result = reassignable_module_var * 2
83 |     return result, other_result
84 | 
85 | 
86 | # Create instances and use variables
87 | dataclass_instance = VariableDataclass(id=1, name="Test")
88 | dataclass_instance.status = "active"  # Reassign dataclass field
89 | 
90 | # Use variables at module level
91 | module_result = module_var + " used at module level"
92 | other_module_result = reassignable_module_var + 30
93 | 
94 | # Create a second dataclass instance with different status
95 | second_dataclass = VariableDataclass(id=2, name="Another Test")
96 | second_dataclass.status = "completed"  # Another reassignment of status
97 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/terraform/test_terraform_basic.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Basic integration tests for the Terraform language server functionality.
 3 | 
 4 | These tests validate the functionality of the language server APIs
 5 | like request_references using the test repository.
 6 | """
 7 | 
 8 | import pytest
 9 | 
10 | from solidlsp import SolidLanguageServer
11 | from solidlsp.ls_config import Language
12 | 
13 | 
14 | @pytest.mark.terraform
15 | class TestLanguageServerBasics:
16 |     """Test basic functionality of the Terraform language server."""
17 | 
18 |     @pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True)
19 |     def test_basic_definition(self, language_server: SolidLanguageServer) -> None:
20 |         """Test basic definition lookup functionality."""
21 |         # Simple test to verify the language server is working
22 |         file_path = "main.tf"
23 |         # Just try to get document symbols - this should work without hanging
24 |         symbols = language_server.request_document_symbols(file_path)
25 |         assert len(symbols) > 0, "Should find at least some symbols in main.tf"
26 | 
27 |     @pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True)
28 |     def test_request_references_aws_instance(self, language_server: SolidLanguageServer) -> None:
29 |         """Test request_references on an aws_instance resource."""
30 |         # Get references to an aws_instance resource in main.tf
31 |         file_path = "main.tf"
32 |         # Find aws_instance resources
33 |         symbols = language_server.request_document_symbols(file_path)
34 |         aws_instance_symbol = next((s for s in symbols[0] if s.get("name") == 'resource "aws_instance" "web_server"'), None)
35 |         if not aws_instance_symbol or "selectionRange" not in aws_instance_symbol:
36 |             raise AssertionError("aws_instance symbol or its selectionRange not found")
37 |         sel_start = aws_instance_symbol["selectionRange"]["start"]
38 |         references = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
39 |         assert len(references) >= 1, "aws_instance should be referenced at least once"
40 | 
41 |     @pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True)
42 |     def test_request_references_variable(self, language_server: SolidLanguageServer) -> None:
43 |         """Test request_references on a variable."""
44 |         # Get references to a variable in variables.tf
45 |         file_path = "variables.tf"
46 |         # Find variable definitions
47 |         symbols = language_server.request_document_symbols(file_path)
48 |         var_symbol = next((s for s in symbols[0] if s.get("name") == 'variable "instance_type"'), None)
49 |         if not var_symbol or "selectionRange" not in var_symbol:
50 |             raise AssertionError("variable symbol or its selectionRange not found")
51 |         sel_start = var_symbol["selectionRange"]["start"]
52 |         references = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
53 |         assert len(references) >= 1, "variable should be referenced at least once"
54 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/elm/test_elm_basic.py:
--------------------------------------------------------------------------------

```python
 1 | import os
 2 | 
 3 | import pytest
 4 | 
 5 | from solidlsp import SolidLanguageServer
 6 | from solidlsp.ls_config import Language
 7 | from solidlsp.ls_utils import SymbolUtils
 8 | 
 9 | 
10 | @pytest.mark.elm
11 | class TestElmLanguageServer:
12 |     @pytest.mark.parametrize("language_server", [Language.ELM], indirect=True)
13 |     def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
14 |         symbols = language_server.request_full_symbol_tree()
15 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "greet"), "greet function not found in symbol tree"
16 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "calculateSum"), "calculateSum function not found in symbol tree"
17 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "formatMessage"), "formatMessage function not found in symbol tree"
18 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "addNumbers"), "addNumbers function not found in symbol tree"
19 | 
20 |     @pytest.mark.parametrize("language_server", [Language.ELM], indirect=True)
21 |     def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
22 |         file_path = os.path.join("Main.elm")
23 |         symbols = language_server.request_document_symbols(file_path)
24 |         greet_symbol = None
25 |         for sym in symbols[0]:
26 |             if sym.get("name") == "greet":
27 |                 greet_symbol = sym
28 |                 break
29 |         assert greet_symbol is not None, "Could not find 'greet' symbol in Main.elm"
30 |         sel_start = greet_symbol["selectionRange"]["start"]
31 |         refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
32 |         assert any("Main.elm" in ref.get("relativePath", "") for ref in refs), "Main.elm should reference greet function"
33 | 
34 |     @pytest.mark.parametrize("language_server", [Language.ELM], indirect=True)
35 |     def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None:
36 |         # Test formatMessage function which is defined in Utils.elm and used in Main.elm
37 |         utils_path = os.path.join("Utils.elm")
38 |         symbols = language_server.request_document_symbols(utils_path)
39 |         formatMessage_symbol = None
40 |         for sym in symbols[0]:
41 |             if sym.get("name") == "formatMessage":
42 |                 formatMessage_symbol = sym
43 |                 break
44 |         assert formatMessage_symbol is not None, "Could not find 'formatMessage' symbol in Utils.elm"
45 | 
46 |         # Get references from the definition in Utils.elm
47 |         sel_start = formatMessage_symbol["selectionRange"]["start"]
48 |         refs = language_server.request_references(utils_path, sel_start["line"], sel_start["character"])
49 | 
50 |         # Verify that we found references
51 |         assert refs, "Expected to find references for formatMessage"
52 | 
53 |         # Verify that at least one reference is in Main.elm (where formatMessage is used)
54 |         assert any("Main.elm" in ref.get("relativePath", "") for ref in refs), "Expected to find usage of formatMessage in Main.elm"
55 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/rust/test_rust_basic.py:
--------------------------------------------------------------------------------

```python
 1 | import os
 2 | 
 3 | import pytest
 4 | 
 5 | from solidlsp import SolidLanguageServer
 6 | from solidlsp.ls_config import Language
 7 | from solidlsp.ls_utils import SymbolUtils
 8 | 
 9 | 
10 | @pytest.mark.rust
11 | class TestRustLanguageServer:
12 |     @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True)
13 |     def test_find_references_raw(self, language_server: SolidLanguageServer) -> None:
14 |         # Directly test the request_references method for the add function
15 |         file_path = os.path.join("src", "lib.rs")
16 |         symbols = language_server.request_document_symbols(file_path)
17 |         add_symbol = None
18 |         for sym in symbols[0]:
19 |             if sym.get("name") == "add":
20 |                 add_symbol = sym
21 |                 break
22 |         assert add_symbol is not None, "Could not find 'add' function symbol in lib.rs"
23 |         sel_start = add_symbol["selectionRange"]["start"]
24 |         refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
25 |         assert any(
26 |             "main.rs" in ref.get("relativePath", "") for ref in refs
27 |         ), "main.rs should reference add (raw, tried all positions in selectionRange)"
28 | 
29 |     @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True)
30 |     def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
31 |         symbols = language_server.request_full_symbol_tree()
32 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main function not found in symbol tree"
33 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add function not found in symbol tree"
34 |         # Add more as needed based on test_repo
35 | 
36 |     @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True)
37 |     def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
38 |         # Find references to 'add' defined in lib.rs, should be referenced from main.rs
39 |         file_path = os.path.join("src", "lib.rs")
40 |         symbols = language_server.request_document_symbols(file_path)
41 |         add_symbol = None
42 |         for sym in symbols[0]:
43 |             if sym.get("name") == "add":
44 |                 add_symbol = sym
45 |                 break
46 |         assert add_symbol is not None, "Could not find 'add' function symbol in lib.rs"
47 |         sel_start = add_symbol["selectionRange"]["start"]
48 |         refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
49 |         assert any(
50 |             "main.rs" in ref.get("relativePath", "") for ref in refs
51 |         ), "main.rs should reference add (tried all positions in selectionRange)"
52 | 
53 |     @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True)
54 |     def test_overview_methods(self, language_server: SolidLanguageServer) -> None:
55 |         symbols = language_server.request_full_symbol_tree()
56 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main missing from overview"
57 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add missing from overview"
58 | 
```

--------------------------------------------------------------------------------
/docs/serena_on_chatgpt.md:
--------------------------------------------------------------------------------

```markdown
  1 | 
  2 | # Connecting Serena MCP Server to ChatGPT via MCPO & Cloudflare Tunnel
  3 | 
  4 | This guide explains how to expose a **locally running Serena MCP server** (powered by MCPO) to the internet using **Cloudflare Tunnel**, and how to connect it to **ChatGPT as a Custom GPT with tool access**.
  5 | 
  6 | Once configured, ChatGPT becomes a powerful **coding agent** with direct access to your codebase, shell, and file system — so **read the security notes carefully**.
  7 | 
  8 | ---
  9 | ## Prerequisites
 10 | 
 11 | Make sure you have [uv](https://docs.astral.sh/uv/getting-started/installation/) 
 12 | and [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) installed.
 13 | 
 14 | ## 1. Start the Serena MCP Server via MCPO
 15 | 
 16 | Run the following command to launch Serena as http server (assuming port 8000):
 17 | 
 18 | ```bash
 19 | uvx mcpo --port 8000 --api-key <YOUR_SECRET_KEY> -- \
 20 |   uvx --from git+https://github.com/oraios/serena \
 21 |   serena start-mcp-server --context chatgpt --project $(pwd)
 22 | ```
 23 | 
 24 | - `--api-key` is required to secure the server.
 25 | - `--project` should point to the root of your codebase.
 26 | 
 27 | You can also use other options, and you don't have to pass `--project` if you want to work on multiple projects
 28 | or want to activate it later. See 
 29 | 
 30 | ```shell
 31 | uvx --from git+https://github.com/oraios/serena serena start-mcp-server --help
 32 | ```
 33 | 
 34 | ---
 35 | 
 36 | ## 2. Expose the Server Using Cloudflare Tunnel
 37 | 
 38 | Run:
 39 | 
 40 | ```bash
 41 | cloudflared tunnel --url http://localhost:8000
 42 | ```
 43 | 
 44 | This will give you a **public HTTPS URL** like:
 45 | 
 46 | ```
 47 | https://serena-agent-tunnel.trycloudflare.com
 48 | ```
 49 | 
 50 | Your server is now securely exposed to the internet.
 51 | 
 52 | ---
 53 | 
 54 | ## 3. Connect It to ChatGPT (Custom GPT)
 55 | 
 56 | ### Steps:
 57 | 
 58 | 1. Go to [ChatGPT → Explore GPTs → Create](https://chat.openai.com/gpts/editor)
 59 | 2. During setup, click **“Add APIs”**
 60 | 3. Set up **API Key authentication** with the auth type as **Bearer** and enter the api key you used to start the MCPO server.
 61 | 4. In the **Schema** section, click on **import from URL** and paste `<cloudflared_url>/openapi.json` with the URL you got from the previous step.
 62 | 5. Add the following line to the top of the imported JSON schema:
 63 |     ```
 64 |      "servers": ["url": "<cloudflared_url>"],
 65 |     ```
 66 |    **Important**: don't include a trailing slash at the end of the URL!
 67 | 
 68 | ChatGPT will read the schema and create functions automatically.
 69 | 
 70 | ---
 71 | 
 72 | ## Security Warning — Read Carefully
 73 | 
 74 | Depending on your configuration and enabled tools, Serena's MCP server may:
 75 | - Execute **arbitrary shell commands**
 76 | - Read, write, and modify **files in your codebase**
 77 | 
 78 | This gives ChatGPT the same powers as a remote developer on your machine.
 79 | 
 80 | ### ⚠️ Key Rules:
 81 | - **NEVER expose your API key**
 82 | - **Only expose this server when needed**, and monitor its use.
 83 | 
 84 | In your project’s `.serena/project.yml` or global config, you can disable tools like:
 85 | 
 86 | ```yaml
 87 | excluded_tools:
 88 |   - execute_shell_command
 89 |   - ...
 90 | read_only: true
 91 | ```
 92 | 
 93 | This is strongly recommended if you want a read-only or safer agent.
 94 | 
 95 | 
 96 | ---
 97 | 
 98 | ## Final Thoughts
 99 | 
100 | With this setup, ChatGPT becomes a coding assistant **running on your local code** — able to index, search, edit, and even run shell commands depending on your configuration.
101 | 
102 | Use responsibly, and keep security in mind.
103 | 
```

--------------------------------------------------------------------------------
/test/resources/repos/ruby/test_repo/variables.rb:
--------------------------------------------------------------------------------

```ruby
  1 | require './models.rb'
  2 | 
  3 | # Global variables for testing references
  4 | $global_counter = 0
  5 | $global_config = {
  6 |   debug: true,
  7 |   timeout: 30
  8 | }
  9 | 
 10 | class DataContainer
 11 |   attr_accessor :status, :data, :metadata
 12 | 
 13 |   def initialize
 14 |     @status = "pending"
 15 |     @data = {}
 16 |     @metadata = {
 17 |       created_at: Time.now,
 18 |       version: "1.0"
 19 |     }
 20 |   end
 21 | 
 22 |   def update_status(new_status)
 23 |     old_status = @status
 24 |     @status = new_status
 25 |     log_status_change(old_status, new_status)
 26 |   end
 27 | 
 28 |   def process_data(input_data)
 29 |     @data = input_data
 30 |     @status = "processing"
 31 |     
 32 |     # Process the data
 33 |     result = @data.transform_values { |v| v.to_s.upcase }
 34 |     @status = "completed"
 35 |     
 36 |     result
 37 |   end
 38 | 
 39 |   def get_metadata_info
 40 |     info = "Status: #{@status}, Version: #{@metadata[:version]}"
 41 |     info += ", Created: #{@metadata[:created_at]}"
 42 |     info
 43 |   end
 44 | 
 45 |   private
 46 | 
 47 |   def log_status_change(old_status, new_status)
 48 |     puts "Status changed from #{old_status} to #{new_status}"
 49 |   end
 50 | end
 51 | 
 52 | class StatusTracker
 53 |   def initialize
 54 |     @tracked_items = []
 55 |   end
 56 | 
 57 |   def add_item(item)
 58 |     @tracked_items << item
 59 |     item.status = "tracked" if item.respond_to?(:status=)
 60 |   end
 61 | 
 62 |   def find_by_status(target_status)
 63 |     @tracked_items.select { |item| item.status == target_status }
 64 |   end
 65 | 
 66 |   def update_all_status(new_status)
 67 |     @tracked_items.each do |item|
 68 |       item.status = new_status if item.respond_to?(:status=)
 69 |     end
 70 |   end
 71 | end
 72 | 
 73 | # Module level variables and functions
 74 | module ProcessingHelper
 75 |   PROCESSING_MODES = ["sync", "async", "batch"].freeze
 76 |   
 77 |   @@instance_count = 0
 78 |   
 79 |   def self.create_processor(mode = "sync")
 80 |     @@instance_count += 1
 81 |     {
 82 |       id: @@instance_count,
 83 |       mode: mode,
 84 |       created_at: Time.now
 85 |     }
 86 |   end
 87 |   
 88 |   def self.get_instance_count
 89 |     @@instance_count
 90 |   end
 91 | end
 92 | 
 93 | # Test instances for reference testing
 94 | dataclass_instance = DataContainer.new
 95 | dataclass_instance.status = "initialized"
 96 | 
 97 | second_dataclass = DataContainer.new  
 98 | second_dataclass.update_status("ready")
 99 | 
100 | tracker = StatusTracker.new
101 | tracker.add_item(dataclass_instance)
102 | tracker.add_item(second_dataclass)
103 | 
104 | # Function that uses the variables
105 | def demonstrate_variable_usage
106 |   puts "Global counter: #{$global_counter}"
107 |   
108 |   container = DataContainer.new
109 |   container.status = "demo"
110 |   
111 |   processor = ProcessingHelper.create_processor("async")
112 |   puts "Created processor #{processor[:id]} in #{processor[:mode]} mode"
113 |   
114 |   container
115 | end
116 | 
117 | # More complex variable interactions
118 | class VariableInteractionTest
119 |   def initialize
120 |     @internal_status = "created"
121 |     @data_containers = []
122 |   end
123 |   
124 |   def add_container(container)
125 |     @data_containers << container
126 |     container.status = "added_to_collection"
127 |     @internal_status = "modified"
128 |   end
129 |   
130 |   def process_all_containers
131 |     @data_containers.each do |container|
132 |       container.status = "batch_processed"
133 |     end
134 |     @internal_status = "processing_complete"
135 |   end
136 |   
137 |   def get_status_summary
138 |     statuses = @data_containers.map(&:status)
139 |     {
140 |       internal: @internal_status,
141 |       containers: statuses,
142 |       count: @data_containers.length
143 |     }
144 |   end
145 | end
146 | 
147 | # Create instances for testing
148 | interaction_test = VariableInteractionTest.new
149 | interaction_test.add_container(dataclass_instance)
150 | interaction_test.add_container(second_dataclass)
```

--------------------------------------------------------------------------------
/test/resources/repos/python/test_repo/test_repo/utils.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Utility functions and classes demonstrating various Python features.
  3 | """
  4 | 
  5 | import logging
  6 | from collections.abc import Callable
  7 | from typing import Any, TypeVar
  8 | 
  9 | # Type variables for generic functions
 10 | T = TypeVar("T")
 11 | U = TypeVar("U")
 12 | 
 13 | 
 14 | def setup_logging(level: str = "INFO") -> logging.Logger:
 15 |     """Set up and return a configured logger"""
 16 |     levels = {
 17 |         "DEBUG": logging.DEBUG,
 18 |         "INFO": logging.INFO,
 19 |         "WARNING": logging.WARNING,
 20 |         "ERROR": logging.ERROR,
 21 |         "CRITICAL": logging.CRITICAL,
 22 |     }
 23 | 
 24 |     logger = logging.getLogger("test_repo")
 25 |     logger.setLevel(levels.get(level.upper(), logging.INFO))
 26 | 
 27 |     handler = logging.StreamHandler()
 28 |     formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
 29 |     handler.setFormatter(formatter)
 30 |     logger.addHandler(handler)
 31 | 
 32 |     return logger
 33 | 
 34 | 
 35 | # Decorator example
 36 | def log_execution(func: Callable) -> Callable:
 37 |     """Decorator to log function execution"""
 38 | 
 39 |     def wrapper(*args, **kwargs):
 40 |         logger = logging.getLogger("test_repo")
 41 |         logger.info(f"Executing function: {func.__name__}")
 42 |         result = func(*args, **kwargs)
 43 |         logger.info(f"Completed function: {func.__name__}")
 44 |         return result
 45 | 
 46 |     return wrapper
 47 | 
 48 | 
 49 | # Higher-order function
 50 | def map_list(items: list[T], mapper: Callable[[T], U]) -> list[U]:
 51 |     """Map a function over a list of items"""
 52 |     return [mapper(item) for item in items]
 53 | 
 54 | 
 55 | # Class with various Python features
 56 | class ConfigManager:
 57 |     """Manages configuration with various access patterns"""
 58 | 
 59 |     _instance = None
 60 | 
 61 |     # Singleton pattern
 62 |     def __new__(cls, *args, **kwargs):
 63 |         if not cls._instance:
 64 |             cls._instance = super().__new__(cls)
 65 |         return cls._instance
 66 | 
 67 |     def __init__(self, initial_config: dict[str, Any] | None = None):
 68 |         if not hasattr(self, "initialized"):
 69 |             self.config = initial_config or {}
 70 |             self.initialized = True
 71 | 
 72 |     def __getitem__(self, key: str) -> Any:
 73 |         """Allow dictionary-like access"""
 74 |         return self.config.get(key)
 75 | 
 76 |     def __setitem__(self, key: str, value: Any) -> None:
 77 |         """Allow dictionary-like setting"""
 78 |         self.config[key] = value
 79 | 
 80 |     @property
 81 |     def debug_mode(self) -> bool:
 82 |         """Property example"""
 83 |         return self.config.get("debug", False)
 84 | 
 85 |     @debug_mode.setter
 86 |     def debug_mode(self, value: bool) -> None:
 87 |         self.config["debug"] = value
 88 | 
 89 | 
 90 | # Context manager example
 91 | class Timer:
 92 |     """Context manager for timing code execution"""
 93 | 
 94 |     def __init__(self, name: str = "Timer"):
 95 |         self.name = name
 96 |         self.start_time = None
 97 |         self.end_time = None
 98 | 
 99 |     def __enter__(self):
100 |         import time
101 | 
102 |         self.start_time = time.time()
103 |         return self
104 | 
105 |     def __exit__(self, exc_type, exc_val, exc_tb):
106 |         import time
107 | 
108 |         self.end_time = time.time()
109 |         print(f"{self.name} took {self.end_time - self.start_time:.6f} seconds")
110 | 
111 | 
112 | # Functions with default arguments
113 | def retry(func: Callable, max_attempts: int = 3, delay: float = 1.0) -> Any:
114 |     """Retry a function with backoff"""
115 |     import time
116 | 
117 |     for attempt in range(max_attempts):
118 |         try:
119 |             return func()
120 |         except Exception as e:
121 |             if attempt == max_attempts - 1:
122 |                 raise e
123 |             time.sleep(delay * (2**attempt))
124 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/markdown/test_markdown_basic.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Basic integration tests for the markdown language server functionality.
 3 | 
 4 | These tests validate the functionality of the language server APIs
 5 | like request_document_symbols using the markdown test repository.
 6 | """
 7 | 
 8 | import pytest
 9 | 
10 | from solidlsp import SolidLanguageServer
11 | from solidlsp.ls_config import Language
12 | 
13 | 
14 | @pytest.mark.markdown
15 | class TestMarkdownLanguageServerBasics:
16 |     """Test basic functionality of the markdown language server."""
17 | 
18 |     @pytest.mark.parametrize("language_server", [Language.MARKDOWN], indirect=True)
19 |     def test_markdown_language_server_initialization(self, language_server: SolidLanguageServer) -> None:
20 |         """Test that markdown language server can be initialized successfully."""
21 |         assert language_server is not None
22 |         assert language_server.language == Language.MARKDOWN
23 | 
24 |     @pytest.mark.parametrize("language_server", [Language.MARKDOWN], indirect=True)
25 |     def test_markdown_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
26 |         """Test request_document_symbols for markdown files."""
27 |         # Test getting symbols from README.md
28 |         all_symbols, _root_symbols = language_server.request_document_symbols("README.md", include_body=False)
29 | 
30 |         # Extract heading symbols (LSP Symbol Kind 15 is String, but marksman uses kind 15 for headings)
31 |         # Note: Different markdown LSPs may use different symbol kinds for headings
32 |         # Marksman typically uses kind 15 (String) for markdown headings
33 |         heading_names = [symbol["name"] for symbol in all_symbols]
34 | 
35 |         # Should detect headings from README.md
36 |         assert "Test Repository" in heading_names or len(all_symbols) > 0, "Should find at least one heading"
37 | 
38 |     @pytest.mark.parametrize("language_server", [Language.MARKDOWN], indirect=True)
39 |     def test_markdown_request_symbols_from_guide(self, language_server: SolidLanguageServer) -> None:
40 |         """Test symbol detection in guide.md file."""
41 |         all_symbols, _root_symbols = language_server.request_document_symbols("guide.md", include_body=False)
42 | 
43 |         # At least some headings should be found
44 |         assert len(all_symbols) > 0, f"Should find headings in guide.md, found {len(all_symbols)}"
45 | 
46 |     @pytest.mark.parametrize("language_server", [Language.MARKDOWN], indirect=True)
47 |     def test_markdown_request_symbols_from_api(self, language_server: SolidLanguageServer) -> None:
48 |         """Test symbol detection in api.md file."""
49 |         all_symbols, _root_symbols = language_server.request_document_symbols("api.md", include_body=False)
50 | 
51 |         # Should detect headings from api.md
52 |         assert len(all_symbols) > 0, f"Should find headings in api.md, found {len(all_symbols)}"
53 | 
54 |     @pytest.mark.parametrize("language_server", [Language.MARKDOWN], indirect=True)
55 |     def test_markdown_request_document_symbols_with_body(self, language_server: SolidLanguageServer) -> None:
56 |         """Test request_document_symbols with body extraction."""
57 |         # Test with include_body=True
58 |         all_symbols, _root_symbols = language_server.request_document_symbols("README.md", include_body=True)
59 | 
60 |         # Should have found some symbols
61 |         assert len(all_symbols) > 0, "Should find symbols in README.md"
62 | 
63 |         # Note: Not all markdown LSPs provide body information for symbols
64 |         # This test is more lenient and just verifies the API works
65 |         assert all_symbols is not None, "Should return symbols even if body extraction is limited"
66 | 
```

--------------------------------------------------------------------------------
/src/serena/tools/config_tools.py:
--------------------------------------------------------------------------------

```python
 1 | import json
 2 | 
 3 | from serena.config.context_mode import SerenaAgentMode
 4 | from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional
 5 | 
 6 | 
 7 | class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
 8 |     """
 9 |     Activates a project by name.
10 |     """
11 | 
12 |     def apply(self, project: str) -> str:
13 |         """
14 |         Activates the project with the given name.
15 | 
16 |         :param project: the name of a registered project to activate or a path to a project directory
17 |         """
18 |         active_project = self.agent.activate_project_from_path_or_name(project)
19 |         if active_project.is_newly_created:
20 |             result_str = (
21 |                 f"Created and activated a new project with name '{active_project.project_name}' at {active_project.project_root}, language: {active_project.project_config.language.value}. "
22 |                 "You can activate this project later by name.\n"
23 |                 f"The project's Serena configuration is in {active_project.path_to_project_yml()}. In particular, you may want to edit the project name and the initial prompt."
24 |             )
25 |         else:
26 |             result_str = f"Activated existing project with name '{active_project.project_name}' at {active_project.project_root}, language: {active_project.project_config.language.value}"
27 | 
28 |         if active_project.project_config.initial_prompt:
29 |             result_str += f"\nAdditional project information:\n {active_project.project_config.initial_prompt}"
30 |         result_str += (
31 |             f"\nAvailable memories:\n {json.dumps(list(self.memories_manager.list_memories()))}"
32 |             + "You should not read these memories directly, but rather use the `read_memory` tool to read them later if needed for the task."
33 |         )
34 |         result_str += f"\nAvailable tools:\n {json.dumps(self.agent.get_active_tool_names())}"
35 |         return result_str
36 | 
37 | 
38 | class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional):
39 |     """
40 |     Removes a project from the Serena configuration.
41 |     """
42 | 
43 |     def apply(self, project_name: str) -> str:
44 |         """
45 |         Removes a project from the Serena configuration.
46 | 
47 |         :param project_name: Name of the project to remove
48 |         """
49 |         self.agent.serena_config.remove_project(project_name)
50 |         return f"Successfully removed project '{project_name}' from configuration."
51 | 
52 | 
53 | class SwitchModesTool(Tool, ToolMarkerOptional):
54 |     """
55 |     Activates modes by providing a list of their names
56 |     """
57 | 
58 |     def apply(self, modes: list[str]) -> str:
59 |         """
60 |         Activates the desired modes, like ["editing", "interactive"] or ["planning", "one-shot"]
61 | 
62 |         :param modes: the names of the modes to activate
63 |         """
64 |         mode_instances = [SerenaAgentMode.load(mode) for mode in modes]
65 |         self.agent.set_modes(mode_instances)
66 | 
67 |         # Inform the Agent about the activated modes and the currently active tools
68 |         result_str = f"Successfully activated modes: {', '.join([mode.name for mode in mode_instances])}" + "\n"
69 |         result_str += "\n".join([mode_instance.prompt for mode_instance in mode_instances]) + "\n"
70 |         result_str += f"Currently active tools: {', '.join(self.agent.get_active_tool_names())}"
71 |         return result_str
72 | 
73 | 
74 | class GetCurrentConfigTool(Tool):
75 |     """
76 |     Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
77 |     """
78 | 
79 |     def apply(self) -> str:
80 |         """
81 |         Print the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
82 |         """
83 |         return self.agent.get_current_config_overview()
84 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/util/test_zip.py:
--------------------------------------------------------------------------------

```python
  1 | import sys
  2 | import zipfile
  3 | from pathlib import Path
  4 | 
  5 | import pytest
  6 | 
  7 | from solidlsp.util.zip import SafeZipExtractor
  8 | 
  9 | 
 10 | @pytest.fixture
 11 | def temp_zip_file(tmp_path: Path) -> Path:
 12 |     """Create a temporary ZIP file for testing."""
 13 |     zip_path = tmp_path / "test.zip"
 14 |     with zipfile.ZipFile(zip_path, "w") as zipf:
 15 |         zipf.writestr("file1.txt", "Hello World 1")
 16 |         zipf.writestr("file2.txt", "Hello World 2")
 17 |         zipf.writestr("folder/file3.txt", "Hello World 3")
 18 |     return zip_path
 19 | 
 20 | 
 21 | def test_extract_all_success(temp_zip_file: Path, tmp_path: Path) -> None:
 22 |     """All files should extract without error."""
 23 |     dest_dir = tmp_path / "extracted"
 24 |     extractor = SafeZipExtractor(temp_zip_file, dest_dir, verbose=False)
 25 |     extractor.extract_all()
 26 | 
 27 |     assert (dest_dir / "file1.txt").read_text() == "Hello World 1"
 28 |     assert (dest_dir / "file2.txt").read_text() == "Hello World 2"
 29 |     assert (dest_dir / "folder" / "file3.txt").read_text() == "Hello World 3"
 30 | 
 31 | 
 32 | def test_include_patterns(temp_zip_file: Path, tmp_path: Path) -> None:
 33 |     """Only files matching include_patterns should be extracted."""
 34 |     dest_dir = tmp_path / "extracted"
 35 |     extractor = SafeZipExtractor(temp_zip_file, dest_dir, verbose=False, include_patterns=["*.txt"])
 36 |     extractor.extract_all()
 37 | 
 38 |     assert (dest_dir / "file1.txt").exists()
 39 |     assert (dest_dir / "file2.txt").exists()
 40 |     assert (dest_dir / "folder" / "file3.txt").exists()
 41 | 
 42 | 
 43 | def test_exclude_patterns(temp_zip_file: Path, tmp_path: Path) -> None:
 44 |     """Files matching exclude_patterns should be skipped."""
 45 |     dest_dir = tmp_path / "extracted"
 46 |     extractor = SafeZipExtractor(temp_zip_file, dest_dir, verbose=False, exclude_patterns=["file2.txt"])
 47 |     extractor.extract_all()
 48 | 
 49 |     assert (dest_dir / "file1.txt").exists()
 50 |     assert not (dest_dir / "file2.txt").exists()
 51 |     assert (dest_dir / "folder" / "file3.txt").exists()
 52 | 
 53 | 
 54 | def test_include_and_exclude_patterns(temp_zip_file: Path, tmp_path: Path) -> None:
 55 |     """Exclude should override include if both match."""
 56 |     dest_dir = tmp_path / "extracted"
 57 |     extractor = SafeZipExtractor(
 58 |         temp_zip_file,
 59 |         dest_dir,
 60 |         verbose=False,
 61 |         include_patterns=["*.txt"],
 62 |         exclude_patterns=["file1.txt"],
 63 |     )
 64 |     extractor.extract_all()
 65 | 
 66 |     assert not (dest_dir / "file1.txt").exists()
 67 |     assert (dest_dir / "file2.txt").exists()
 68 |     assert (dest_dir / "folder" / "file3.txt").exists()
 69 | 
 70 | 
 71 | def test_skip_on_error(monkeypatch, temp_zip_file: Path, tmp_path: Path) -> None:
 72 |     """Should skip a file that raises an error and continue extracting others."""
 73 |     dest_dir = tmp_path / "extracted"
 74 | 
 75 |     original_open = zipfile.ZipFile.open
 76 | 
 77 |     def failing_open(self, member, *args, **kwargs):
 78 |         if member.filename == "file2.txt":
 79 |             raise OSError("Simulated failure")
 80 |         return original_open(self, member, *args, **kwargs)
 81 | 
 82 |     # Patch the method on the class, not on an instance
 83 |     monkeypatch.setattr(zipfile.ZipFile, "open", failing_open)
 84 | 
 85 |     extractor = SafeZipExtractor(temp_zip_file, dest_dir, verbose=False)
 86 |     extractor.extract_all()
 87 | 
 88 |     assert (dest_dir / "file1.txt").exists()
 89 |     assert not (dest_dir / "file2.txt").exists()
 90 |     assert (dest_dir / "folder" / "file3.txt").exists()
 91 | 
 92 | 
 93 | @pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows-only test")
 94 | def test_long_path_normalization(temp_zip_file: Path, tmp_path: Path) -> None:
 95 |     r"""Ensure _normalize_path adds \\?\\ prefix on Windows."""
 96 |     dest_dir = tmp_path / ("a" * 250)  # Simulate long path
 97 |     extractor = SafeZipExtractor(temp_zip_file, dest_dir, verbose=False)
 98 |     norm_path = extractor._normalize_path(dest_dir / "file.txt")
 99 |     assert str(norm_path).startswith("\\\\?\\")
100 | 
```

--------------------------------------------------------------------------------
/roadmap.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Roadmap
 2 | 
 3 | This document gives an overview of the ongoing and future development of Serena.
 4 | If you have a proposal or want to discuss something, feel free to open a discussion
 5 | on Github. For a summary of the past development, see the [changelog](/CHANGELOG.md).
 6 | 
 7 | Want to see us reach our goals faster? You can help out with an issue, start a discussion, or 
 8 | inform us about funding opportunities so that we can devote more time to the project.
 9 | 
10 | ## Overall Goals
11 | 
12 | Serena has the potential to be the go-to tool for most LLM coding tasks, since it is 
13 | unique in its ability to be used as MCP Server in any kind of environment
14 | while still being a capable agent. We want to achieve the following goals in terms of functionality:
15 | 
16 | 1. Top performance (comparable to API-based coding agents) when used through official (free) clients like Claude Desktop.
17 | 1. Lowering API costs and potentially improving performance of coding clients (Claude Code, Codex, Cline, Roo, Cursor/Windsurf/VSCode etc).
18 | 1. Transparency and simplicity of use. Achieved through the dashboard/logging GUI.
19 | 1. Integrations with major frameworks that don't accept MCP. Usable as a library.
20 | 
21 | Apart from the functional goals, we have the goal of having great code design, so that Serena can be viewed
22 | as a reference for how to implement MCP Servers. Such projects are an emerging technology, and
23 | best practices are yet to be determined. We will share our experiences in [lessons learned](/lessons_learned.md).
24 | 
25 | 
26 | ## Immediate/Ongoing
27 | 
28 | - Support for projects using multiple programming languages.
29 | - Evaluate whether `ReplaceLinesTool` can be removed in favor of a more reliable and performant editing approach.
30 | - Generally experiment with various approaches to editing tools
31 | - Manual evaluation on selected tasks from SWE-verified
32 | - Manual evaluation of cost-lowering and performance when used within popular non-MCP agents
33 | - Improvements in prompts, in particular giving examples and extending modes and contexts
34 | 
35 | ## Upcoming
36 | 
37 | - Publishing Serena as a package that can also be used as library
38 | - Use linting and type-hierarchy from the LSP in tools
39 | - Tools for refactoring (rename, move) - speculative, maybe won't do this.
40 | - Tracking edits and rolling them back with the dashboard
41 | - Improve configurability and safety of shell tool. Maybe autogeneration of tools from a list of commands and descriptions.
42 | - Transparent comparison with DesktopCommander and ...
43 | - Automatic evaluation using OpenHands, submission to SWE-Bench
44 | - Evaluation whether incorporating other MCPs increases performance or usability (memory bank is a candidate)
45 | - More documentation and best practices
46 | 
47 | ## Stretch
48 | 
49 | - Allow for sandboxing and parallel instances of Serena, maybe use openhands or codex for that
50 | - Incorporate a verifier model or generally a second model (maybe for applying edits) as a tool.
51 | - Building on the above, allow for the second model itself to be reachable through an MCP server, so it can be used for free
52 | - Tracking edits performed with shell tools
53 | 
54 | ## Beyond Serena
55 | 
56 | The technologies and approaches taken in Serena can be used for various research and service ideas. Some thought that we had are:
57 | 
58 | - PR and issue assistant working with GitHub, similar to how [OpenHands](https://github.com/All-Hands-AI/OpenHands) 
59 |   and [qodo](https://github.com/qodo-ai/pr-agent) operate. Should be callable through @serena
60 | - Tuning a coding LLM with Serena's tools with RL on one-shot tasks. We would need compute-funding for that
61 | - Develop a web app to quantitatively compare the performance of various agents by scraping PRs and manually crafted metadata.
62 |   The main metric for coding agents should be *developer experience*, and that is hard to grasp and is poorly correlated with
63 |   performance on current benchmarks.
```

--------------------------------------------------------------------------------
/src/interprompt/prompt_factory.py:
--------------------------------------------------------------------------------

```python
 1 | import logging
 2 | import os
 3 | from typing import Any
 4 | 
 5 | from .multilang_prompt import DEFAULT_LANG_CODE, LanguageFallbackMode, MultiLangPromptCollection, PromptList
 6 | 
 7 | log = logging.getLogger(__name__)
 8 | 
 9 | 
10 | class PromptFactoryBase:
11 |     """Base class for auto-generated prompt factory classes."""
12 | 
13 |     def __init__(self, prompts_dir: str | list[str], lang_code: str = DEFAULT_LANG_CODE, fallback_mode=LanguageFallbackMode.EXCEPTION):
14 |         """
15 |         :param prompts_dir: the directory containing the prompt templates and prompt lists.
16 |             If a list is provided, will look for prompt templates in the dirs from left to right
17 |             (first one containing the desired template wins).
18 |         :param lang_code: the language code to use for retrieving the prompt templates and prompt lists.
19 |             Leave as `default` for single-language use cases.
20 |         :param fallback_mode: the fallback mode to use when a prompt template or prompt list is not found for the requested language.
21 |             Irrelevant for single-language use cases.
22 |         """
23 |         self.lang_code = lang_code
24 |         self._prompt_collection = MultiLangPromptCollection(prompts_dir, fallback_mode=fallback_mode)
25 | 
26 |     def _render_prompt(self, prompt_name: str, params: dict[str, Any]) -> str:
27 |         del params["self"]
28 |         return self._prompt_collection.render_prompt_template(prompt_name, params, lang_code=self.lang_code)
29 | 
30 |     def _get_prompt_list(self, prompt_name: str) -> PromptList:
31 |         return self._prompt_collection.get_prompt_list(prompt_name, self.lang_code)
32 | 
33 | 
34 | def autogenerate_prompt_factory_module(prompts_dir: str, target_module_path: str) -> None:
35 |     """
36 |     Auto-generates a prompt factory module for the given prompt directory.
37 |     The generated `PromptFactory` class is meant to be the central entry class for retrieving and rendering prompt templates and prompt
38 |     lists in your application.
39 |     It will contain one method per prompt template and prompt list, and is useful for both single- and multi-language use cases.
40 | 
41 |     :param prompts_dir: the directory containing the prompt templates and prompt lists
42 |     :param target_module_path: the path to the target module file (.py). Important: The module will be overwritten!
43 |     """
44 |     generated_code = """
45 | # ruff: noqa
46 | # black: skip
47 | # mypy: ignore-errors
48 | 
49 | # NOTE: This module is auto-generated from interprompt.autogenerate_prompt_factory_module, do not edit manually!
50 | 
51 | from interprompt.multilang_prompt import PromptList
52 | from interprompt.prompt_factory import PromptFactoryBase
53 | from typing import Any
54 | 
55 | 
56 | class PromptFactory(PromptFactoryBase):
57 |     \"""
58 |     A class for retrieving and rendering prompt templates and prompt lists.
59 |     \"""
60 | """
61 |     # ---- add methods based on prompt template names and parameters and prompt list names ----
62 |     prompt_collection = MultiLangPromptCollection(prompts_dir)
63 | 
64 |     for template_name in prompt_collection.get_prompt_template_names():
65 |         template_parameters = prompt_collection.get_prompt_template_parameters(template_name)
66 |         if len(template_parameters) == 0:
67 |             method_params_str = ""
68 |         else:
69 |             method_params_str = ", *, " + ", ".join([f"{param}: Any" for param in template_parameters])
70 |         generated_code += f"""
71 |     def create_{template_name}(self{method_params_str}) -> str:
72 |         return self._render_prompt('{template_name}', locals())
73 | """
74 |     for prompt_list_name in prompt_collection.get_prompt_list_names():
75 |         generated_code += f"""
76 |     def get_list_{prompt_list_name}(self) -> PromptList:
77 |         return self._get_prompt_list('{prompt_list_name}')
78 | """
79 |     os.makedirs(os.path.dirname(target_module_path), exist_ok=True)
80 |     with open(target_module_path, "w", encoding="utf-8") as f:
81 |         f.write(generated_code)
82 |     log.info(f"Prompt factory generated successfully in {target_module_path}")
83 | 
```

--------------------------------------------------------------------------------
/src/solidlsp/lsp_protocol_handler/server.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | This file provides the implementation of the JSON-RPC client, that launches and
  3 | communicates with the language server.
  4 | 
  5 | The initial implementation of this file was obtained from
  6 | https://github.com/predragnikolic/OLSP under the MIT License with the following terms:
  7 | 
  8 | MIT License
  9 | 
 10 | Copyright (c) 2023 Предраг Николић
 11 | 
 12 | Permission is hereby granted, free of charge, to any person obtaining a copy
 13 | of this software and associated documentation files (the "Software"), to deal
 14 | in the Software without restriction, including without limitation the rights
 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 16 | copies of the Software, and to permit persons to whom the Software is
 17 | furnished to do so, subject to the following conditions:
 18 | 
 19 | The above copyright notice and this permission notice shall be included in all
 20 | copies or substantial portions of the Software.
 21 | 
 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 28 | SOFTWARE.
 29 | """
 30 | 
 31 | import dataclasses
 32 | import json
 33 | import logging
 34 | import os
 35 | from typing import Any, Union
 36 | 
 37 | from .lsp_types import ErrorCodes
 38 | 
 39 | StringDict = dict[str, Any]
 40 | PayloadLike = Union[list[StringDict], StringDict, None]
 41 | CONTENT_LENGTH = "Content-Length: "
 42 | ENCODING = "utf-8"
 43 | log = logging.getLogger(__name__)
 44 | 
 45 | 
 46 | @dataclasses.dataclass
 47 | class ProcessLaunchInfo:
 48 |     """
 49 |     This class is used to store the information required to launch a process.
 50 |     """
 51 | 
 52 |     # The command to launch the process
 53 |     cmd: str | list[str]
 54 | 
 55 |     # The environment variables to set for the process
 56 |     env: dict[str, str] = dataclasses.field(default_factory=dict)
 57 | 
 58 |     # The working directory for the process
 59 |     cwd: str = os.getcwd()
 60 | 
 61 | 
 62 | class LSPError(Exception):
 63 |     def __init__(self, code: ErrorCodes, message: str) -> None:
 64 |         super().__init__(message)
 65 |         self.code = code
 66 | 
 67 |     def to_lsp(self) -> StringDict:
 68 |         return {"code": self.code, "message": super().__str__()}
 69 | 
 70 |     @classmethod
 71 |     def from_lsp(cls, d: StringDict) -> "LSPError":
 72 |         return LSPError(d["code"], d["message"])
 73 | 
 74 |     def __str__(self) -> str:
 75 |         return f"{super().__str__()} ({self.code})"
 76 | 
 77 | 
 78 | def make_response(request_id: Any, params: PayloadLike) -> StringDict:
 79 |     return {"jsonrpc": "2.0", "id": request_id, "result": params}
 80 | 
 81 | 
 82 | def make_error_response(request_id: Any, err: LSPError) -> StringDict:
 83 |     return {"jsonrpc": "2.0", "id": request_id, "error": err.to_lsp()}
 84 | 
 85 | 
 86 | def make_notification(method: str, params: PayloadLike) -> StringDict:
 87 |     return {"jsonrpc": "2.0", "method": method, "params": params}
 88 | 
 89 | 
 90 | def make_request(method: str, request_id: Any, params: PayloadLike) -> StringDict:
 91 |     return {"jsonrpc": "2.0", "method": method, "id": request_id, "params": params}
 92 | 
 93 | 
 94 | class StopLoopException(Exception):
 95 |     pass
 96 | 
 97 | 
 98 | def create_message(payload: PayloadLike):
 99 |     body = json.dumps(payload, check_circular=False, ensure_ascii=False, separators=(",", ":")).encode(ENCODING)
100 |     return (
101 |         f"Content-Length: {len(body)}\r\n".encode(ENCODING),
102 |         "Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n".encode(ENCODING),
103 |         body,
104 |     )
105 | 
106 | 
107 | class MessageType:
108 |     error = 1
109 |     warning = 2
110 |     info = 3
111 |     log = 4
112 | 
113 | 
114 | def content_length(line: bytes) -> int | None:
115 |     if line.startswith(b"Content-Length: "):
116 |         _, value = line.split(b"Content-Length: ")
117 |         value = value.strip()
118 |         try:
119 |             return int(value)
120 |         except ValueError:
121 |             raise ValueError(f"Invalid Content-Length header: {value}")
122 |     return None
123 | 
```

--------------------------------------------------------------------------------
/.serena/memories/serena_repository_structure.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Serena Repository Structure
  2 | 
  3 | ## Overview
  4 | Serena is a multi-language code assistant that combines two main components:
  5 | 1. **Serena Core** - The main agent framework with tools and MCP server
  6 | 2. **SolidLSP** - A unified Language Server Protocol wrapper for multiple programming languages
  7 | 
  8 | ## Top-Level Structure
  9 | 
 10 | ```
 11 | serena/
 12 | ├── src/                          # Main source code
 13 | │   ├── serena/                   # Serena agent framework
 14 | │   ├── solidlsp/                 # LSP wrapper library  
 15 | │   └── interprompt/              # Multi-language prompt templates
 16 | ├── test/                         # Test suites
 17 | │   ├── serena/                   # Serena agent tests
 18 | │   ├── solidlsp/                 # Language server tests
 19 | │   └── resources/repos/          # Test repositories for each language
 20 | ├── scripts/                      # Build and utility scripts
 21 | ├── resources/                    # Static resources and configurations
 22 | ├── pyproject.toml               # Python project configuration
 23 | ├── README.md                    # Project documentation
 24 | └── CHANGELOG.md                 # Version history
 25 | ```
 26 | 
 27 | ## Source Code Organization
 28 | 
 29 | ### Serena Core (`src/serena/`)
 30 | - **`agent.py`** - Main SerenaAgent class that orchestrates everything
 31 | - **`tools/`** - MCP tools for file operations, symbols, memory, etc.
 32 |   - `file_tools.py` - File system operations (read, write, search)
 33 |   - `symbol_tools.py` - Symbol-based code operations (find, edit)
 34 |   - `memory_tools.py` - Knowledge persistence and retrieval
 35 |   - `config_tools.py` - Project and mode management
 36 |   - `workflow_tools.py` - Onboarding and meta-operations
 37 | - **`config/`** - Configuration management
 38 |   - `serena_config.py` - Main configuration classes
 39 |   - `context_mode.py` - Context and mode definitions
 40 | - **`util/`** - Utility modules
 41 | - **`mcp.py`** - MCP server implementation
 42 | - **`cli.py`** - Command-line interface
 43 | 
 44 | ### SolidLSP (`src/solidlsp/`)
 45 | - **`ls.py`** - Main SolidLanguageServer class
 46 | - **`language_servers/`** - Language-specific implementations
 47 |   - `csharp_language_server.py` - C# (Microsoft.CodeAnalysis.LanguageServer)
 48 |   - `python_server.py` - Python (Pyright)
 49 |   - `typescript_language_server.py` - TypeScript
 50 |   - `rust_analyzer.py` - Rust
 51 |   - `gopls.py` - Go
 52 |   - And many more...
 53 | - **`ls_config.py`** - Language server configuration
 54 | - **`ls_types.py`** - LSP type definitions
 55 | - **`ls_utils.py`** - Utilities for working with LSP data
 56 | 
 57 | ### Interprompt (`src/interprompt/`)
 58 | - Multi-language prompt template system
 59 | - Jinja2-based templating with language fallbacks
 60 | 
 61 | ## Test Structure
 62 | 
 63 | ### Language Server Tests (`test/solidlsp/`)
 64 | Each language has its own test directory:
 65 | ```
 66 | test/solidlsp/
 67 | ├── csharp/
 68 | │   └── test_csharp_basic.py
 69 | ├── python/
 70 | │   └── test_python_basic.py
 71 | ├── typescript/
 72 | │   └── test_typescript_basic.py
 73 | └── ...
 74 | ```
 75 | 
 76 | ### Test Resources (`test/resources/repos/`)
 77 | Contains minimal test projects for each language:
 78 | ```
 79 | test/resources/repos/
 80 | ├── csharp/test_repo/
 81 | │   ├── serena.sln
 82 | │   ├── TestProject.csproj
 83 | │   ├── Program.cs
 84 | │   └── Models/Person.cs
 85 | ├── python/test_repo/
 86 | ├── typescript/test_repo/
 87 | └── ...
 88 | ```
 89 | 
 90 | ### Test Infrastructure
 91 | - **`test/conftest.py`** - Shared test fixtures and utilities
 92 | - **`create_ls()`** function - Creates language server instances for testing
 93 | - **`language_server` fixture** - Parametrized fixture for multi-language tests
 94 | 
 95 | ## Key Configuration Files
 96 | 
 97 | - **`pyproject.toml`** - Python dependencies, build config, and tool settings
 98 | - **`.serena/`** directories - Project-specific Serena configuration and memories
 99 | - **`CLAUDE.md`** - Instructions for AI assistants working on the project
100 | 
101 | ## Dependencies Management
102 | 
103 | The project uses modern Python tooling:
104 | - **uv** for fast dependency resolution and virtual environments
105 | - **pytest** for testing with language-specific markers (`@pytest.mark.csharp`)
106 | - **ruff** for linting and formatting
107 | - **mypy** for type checking
108 | 
109 | ## Build and Development
110 | 
111 | - **Docker support** - Full containerized development environment
112 | - **GitHub Actions** - CI/CD with language server testing
113 | - **Development scripts** in `scripts/` directory
```

--------------------------------------------------------------------------------
/src/solidlsp/language_servers/omnisharp/workspace_did_change_configuration.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |     "RoslynExtensionsOptions": {
  3 |         "EnableDecompilationSupport": false,
  4 |         "EnableAnalyzersSupport": true,
  5 |         "EnableImportCompletion": true,
  6 |         "EnableAsyncCompletion": false,
  7 |         "DocumentAnalysisTimeoutMs": 30000,
  8 |         "DiagnosticWorkersThreadCount": 18,
  9 |         "AnalyzeOpenDocumentsOnly": true,
 10 |         "InlayHintsOptions": {
 11 |             "EnableForParameters": false,
 12 |             "ForLiteralParameters": false,
 13 |             "ForIndexerParameters": false,
 14 |             "ForObjectCreationParameters": false,
 15 |             "ForOtherParameters": false,
 16 |             "SuppressForParametersThatDifferOnlyBySuffix": false,
 17 |             "SuppressForParametersThatMatchMethodIntent": false,
 18 |             "SuppressForParametersThatMatchArgumentName": false,
 19 |             "EnableForTypes": false,
 20 |             "ForImplicitVariableTypes": false,
 21 |             "ForLambdaParameterTypes": false,
 22 |             "ForImplicitObjectCreation": false
 23 |         },
 24 |         "LocationPaths": null
 25 |     },
 26 |     "FormattingOptions": {
 27 |         "OrganizeImports": false,
 28 |         "EnableEditorConfigSupport": true,
 29 |         "NewLine": "\n",
 30 |         "UseTabs": false,
 31 |         "TabSize": 4,
 32 |         "IndentationSize": 4,
 33 |         "SpacingAfterMethodDeclarationName": false,
 34 |         "SeparateImportDirectiveGroups": false,
 35 |         "SpaceWithinMethodDeclarationParenthesis": false,
 36 |         "SpaceBetweenEmptyMethodDeclarationParentheses": false,
 37 |         "SpaceAfterMethodCallName": false,
 38 |         "SpaceWithinMethodCallParentheses": false,
 39 |         "SpaceBetweenEmptyMethodCallParentheses": false,
 40 |         "SpaceAfterControlFlowStatementKeyword": true,
 41 |         "SpaceWithinExpressionParentheses": false,
 42 |         "SpaceWithinCastParentheses": false,
 43 |         "SpaceWithinOtherParentheses": false,
 44 |         "SpaceAfterCast": false,
 45 |         "SpaceBeforeOpenSquareBracket": false,
 46 |         "SpaceBetweenEmptySquareBrackets": false,
 47 |         "SpaceWithinSquareBrackets": false,
 48 |         "SpaceAfterColonInBaseTypeDeclaration": true,
 49 |         "SpaceAfterComma": true,
 50 |         "SpaceAfterDot": false,
 51 |         "SpaceAfterSemicolonsInForStatement": true,
 52 |         "SpaceBeforeColonInBaseTypeDeclaration": true,
 53 |         "SpaceBeforeComma": false,
 54 |         "SpaceBeforeDot": false,
 55 |         "SpaceBeforeSemicolonsInForStatement": false,
 56 |         "SpacingAroundBinaryOperator": "single",
 57 |         "IndentBraces": false,
 58 |         "IndentBlock": true,
 59 |         "IndentSwitchSection": true,
 60 |         "IndentSwitchCaseSection": true,
 61 |         "IndentSwitchCaseSectionWhenBlock": true,
 62 |         "LabelPositioning": "oneLess",
 63 |         "WrappingPreserveSingleLine": true,
 64 |         "WrappingKeepStatementsOnSingleLine": true,
 65 |         "NewLinesForBracesInTypes": true,
 66 |         "NewLinesForBracesInMethods": true,
 67 |         "NewLinesForBracesInProperties": true,
 68 |         "NewLinesForBracesInAccessors": true,
 69 |         "NewLinesForBracesInAnonymousMethods": true,
 70 |         "NewLinesForBracesInControlBlocks": true,
 71 |         "NewLinesForBracesInAnonymousTypes": true,
 72 |         "NewLinesForBracesInObjectCollectionArrayInitializers": true,
 73 |         "NewLinesForBracesInLambdaExpressionBody": true,
 74 |         "NewLineForElse": true,
 75 |         "NewLineForCatch": true,
 76 |         "NewLineForFinally": true,
 77 |         "NewLineForMembersInObjectInit": true,
 78 |         "NewLineForMembersInAnonymousTypes": true,
 79 |         "NewLineForClausesInQuery": true
 80 |     },
 81 |     "FileOptions": {
 82 |         "SystemExcludeSearchPatterns": [
 83 |             "**/node_modules/**/*",
 84 |             "**/bin/**/*",
 85 |             "**/obj/**/*",
 86 |             "**/.git/**/*",
 87 |             "**/.git",
 88 |             "**/.svn",
 89 |             "**/.hg",
 90 |             "**/CVS",
 91 |             "**/.DS_Store",
 92 |             "**/Thumbs.db"
 93 |         ],
 94 |         "ExcludeSearchPatterns": []
 95 |     },
 96 |     "RenameOptions": {
 97 |         "RenameOverloads": false,
 98 |         "RenameInStrings": false,
 99 |         "RenameInComments": false
100 |     },
101 |     "ImplementTypeOptions": {
102 |         "InsertionBehavior": 0,
103 |         "PropertyGenerationBehavior": 0
104 |     },
105 |     "DotNetCliOptions": {
106 |         "LocationPaths": null
107 |     },
108 |     "Plugins": {
109 |         "LocationPaths": null
110 |     }
111 | }
```

--------------------------------------------------------------------------------
/test/solidlsp/perl/test_perl_basic.py:
--------------------------------------------------------------------------------

```python
 1 | import platform
 2 | from pathlib import Path
 3 | 
 4 | import pytest
 5 | 
 6 | from solidlsp import SolidLanguageServer
 7 | from solidlsp.ls_config import Language
 8 | 
 9 | 
10 | @pytest.mark.perl
11 | @pytest.mark.skipif(platform.system() == "Windows", reason="Perl::LanguageServer does not support native Windows operation")
12 | class TestPerlLanguageServer:
13 |     """
14 |     Tests for Perl::LanguageServer integration.
15 | 
16 |     Perl::LanguageServer provides comprehensive LSP support for Perl including:
17 |     - Document symbols (functions, variables)
18 |     - Go to definition (including cross-file)
19 |     - Find references (including cross-file) - this was not available in PLS
20 |     """
21 | 
22 |     @pytest.mark.parametrize("language_server", [Language.PERL], indirect=True)
23 |     @pytest.mark.parametrize("repo_path", [Language.PERL], indirect=True)
24 |     def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
25 |         """Test that the language server starts and stops successfully."""
26 |         # The fixture already handles start and stop
27 |         assert language_server.is_running()
28 |         assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
29 | 
30 |     @pytest.mark.parametrize("language_server", [Language.PERL], indirect=True)
31 |     def test_document_symbols(self, language_server: SolidLanguageServer) -> None:
32 |         """Test that document symbols are correctly identified."""
33 |         # Request document symbols
34 |         all_symbols, _ = language_server.request_document_symbols("main.pl", include_body=False)
35 | 
36 |         assert all_symbols, "Expected to find symbols in main.pl"
37 |         assert len(all_symbols) > 0, "Expected at least one symbol"
38 | 
39 |         # DEBUG: Print all symbols
40 |         print("\n=== All symbols in main.pl ===")
41 |         for s in all_symbols:
42 |             line = s.get("range", {}).get("start", {}).get("line", "?")
43 |             print(f"Line {line}: {s.get('name')} (kind={s.get('kind')})")
44 | 
45 |         # Check that we can find function symbols
46 |         function_symbols = [s for s in all_symbols if s.get("kind") == 12]  # 12 = Function/Method
47 |         assert len(function_symbols) >= 2, f"Expected at least 2 functions (greet, use_helper_function), found {len(function_symbols)}"
48 | 
49 |         function_names = [s.get("name") for s in function_symbols]
50 |         assert "greet" in function_names, f"Expected 'greet' function in symbols, found: {function_names}"
51 |         assert "use_helper_function" in function_names, f"Expected 'use_helper_function' in symbols, found: {function_names}"
52 | 
53 |     # @pytest.mark.skip(reason="Perl::LanguageServer cross-file definition tracking needs configuration")
54 |     @pytest.mark.parametrize("language_server", [Language.PERL], indirect=True)
55 |     def test_find_definition_across_files(self, language_server: SolidLanguageServer) -> None:
56 |         definition_location_list = language_server.request_definition("main.pl", 17, 0)
57 | 
58 |         assert len(definition_location_list) == 1
59 |         definition_location = definition_location_list[0]
60 |         print(f"Found definition: {definition_location}")
61 |         assert definition_location["uri"].endswith("helper.pl")
62 |         assert definition_location["range"]["start"]["line"] == 4  # add method on line 2 (0-indexed 1)
63 | 
64 |     @pytest.mark.parametrize("language_server", [Language.PERL], indirect=True)
65 |     def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None:
66 |         """Test finding references to a function across multiple files."""
67 |         reference_locations = language_server.request_references("helper.pl", 4, 5)
68 | 
69 |         assert len(reference_locations) >= 2, f"Expected at least 2 references to helper_function, found {len(reference_locations)}"
70 | 
71 |         main_pl_refs = [ref for ref in reference_locations if ref["uri"].endswith("main.pl")]
72 |         assert len(main_pl_refs) >= 2, f"Expected at least 2 references in main.pl, found {len(main_pl_refs)}"
73 | 
74 |         main_pl_lines = sorted([ref["range"]["start"]["line"] for ref in main_pl_refs])
75 |         assert 17 in main_pl_lines, f"Expected reference at line 18 (0-indexed 17), found: {main_pl_lines}"
76 |         assert 20 in main_pl_lines, f"Expected reference at line 21 (0-indexed 20), found: {main_pl_lines}"
77 | 
```

--------------------------------------------------------------------------------
/src/solidlsp/util/zip.py:
--------------------------------------------------------------------------------

```python
  1 | import fnmatch
  2 | import logging
  3 | import os
  4 | import sys
  5 | import zipfile
  6 | from pathlib import Path
  7 | from typing import Optional
  8 | 
  9 | log = logging.getLogger(__name__)
 10 | 
 11 | 
 12 | class SafeZipExtractor:
 13 |     """
 14 |     A utility class for extracting ZIP archives safely.
 15 | 
 16 |     Features:
 17 |     - Handles long file paths on Windows
 18 |     - Skips files that fail to extract, continuing with the rest
 19 |     - Creates necessary directories automatically
 20 |     - Optional include/exclude pattern filters
 21 |     """
 22 | 
 23 |     def __init__(
 24 |         self,
 25 |         archive_path: Path,
 26 |         extract_dir: Path,
 27 |         verbose: bool = True,
 28 |         include_patterns: Optional[list[str]] = None,
 29 |         exclude_patterns: Optional[list[str]] = None,
 30 |     ) -> None:
 31 |         """
 32 |         Initialize the SafeZipExtractor.
 33 | 
 34 |         :param archive_path: Path to the ZIP archive file
 35 |         :param extract_dir: Directory where files will be extracted
 36 |         :param verbose: Whether to log status messages
 37 |         :param include_patterns: List of glob patterns for files to extract (None = all files)
 38 |         :param exclude_patterns: List of glob patterns for files to skip
 39 |         """
 40 |         self.archive_path = Path(archive_path)
 41 |         self.extract_dir = Path(extract_dir)
 42 |         self.verbose = verbose
 43 |         self.include_patterns = include_patterns or []
 44 |         self.exclude_patterns = exclude_patterns or []
 45 | 
 46 |     def extract_all(self) -> None:
 47 |         """
 48 |         Extract all files from the archive, skipping any that fail.
 49 |         """
 50 |         if not self.archive_path.exists():
 51 |             raise FileNotFoundError(f"Archive not found: {self.archive_path}")
 52 | 
 53 |         if self.verbose:
 54 |             log.info(f"Extracting from: {self.archive_path} to {self.extract_dir}")
 55 | 
 56 |         with zipfile.ZipFile(self.archive_path, "r") as zip_ref:
 57 |             for member in zip_ref.infolist():
 58 |                 if self._should_extract(member.filename):
 59 |                     self._extract_member(zip_ref, member)
 60 |                 elif self.verbose:
 61 |                     log.info(f"Skipped: {member.filename}")
 62 | 
 63 |     def _should_extract(self, filename: str) -> bool:
 64 |         """
 65 |         Determine whether a file should be extracted based on include/exclude patterns.
 66 | 
 67 |         :param filename: The file name from the archive
 68 |         :return: True if the file should be extracted
 69 |         """
 70 |         # If include_patterns is set, only extract if it matches at least one pattern
 71 |         if self.include_patterns:
 72 |             if not any(fnmatch.fnmatch(filename, pattern) for pattern in self.include_patterns):
 73 |                 return False
 74 | 
 75 |         # If exclude_patterns is set, skip if it matches any pattern
 76 |         if self.exclude_patterns:
 77 |             if any(fnmatch.fnmatch(filename, pattern) for pattern in self.exclude_patterns):
 78 |                 return False
 79 | 
 80 |         return True
 81 | 
 82 |     def _extract_member(self, zip_ref: zipfile.ZipFile, member: zipfile.ZipInfo) -> None:
 83 |         """
 84 |         Extract a single member from the archive with error handling.
 85 | 
 86 |         :param zip_ref: Open ZipFile object
 87 |         :param member: ZipInfo object representing the file
 88 |         """
 89 |         try:
 90 |             target_path = self.extract_dir / member.filename
 91 | 
 92 |             # Ensure directory structure exists
 93 |             target_path.parent.mkdir(parents=True, exist_ok=True)
 94 | 
 95 |             # Handle long paths on Windows
 96 |             final_path = self._normalize_path(target_path)
 97 | 
 98 |             # Extract file
 99 |             with zip_ref.open(member) as source, open(final_path, "wb") as target:
100 |                 target.write(source.read())
101 | 
102 |             if self.verbose:
103 |                 log.info(f"Extracted: {member.filename}")
104 | 
105 |         except Exception as e:
106 |             log.error(f"Failed to extract {member.filename}: {e}")
107 | 
108 |     @staticmethod
109 |     def _normalize_path(path: Path) -> Path:
110 |         """
111 |         Adjust path to handle long paths on Windows.
112 | 
113 |         :param path: Original path
114 |         :return: Normalized path
115 |         """
116 |         if sys.platform.startswith("win"):
117 |             return Path(rf"\\?\{os.path.abspath(path)}")
118 |         return path
119 | 
120 | 
121 | # Example usage:
122 | # extractor = SafeZipExtractor(
123 | #     archive_path=Path("file.nupkg"),
124 | #     extract_dir=Path("extract_dir"),
125 | #     include_patterns=["*.dll", "*.xml"],
126 | #     exclude_patterns=["*.pdb"]
127 | # )
128 | # extractor.extract_all()
129 | 
```

--------------------------------------------------------------------------------
/.serena/project.yml:
--------------------------------------------------------------------------------

```yaml
 1 | # language of the project (csharp, python, rust, java, typescript, javascript, go, cpp, or ruby)
 2 | # Special requirements:
 3 | #  * csharp: Requires the presence of a .sln file in the project folder.
 4 | language: python
 5 | 
 6 | # whether to use the project's gitignore file to ignore files
 7 | # Added on 2025-04-07
 8 | ignore_all_files_in_gitignore: true
 9 | # list of additional paths to ignore
10 | # same syntax as gitignore, so you can use * and **
11 | # Was previously called `ignored_dirs`, please update your config if you are using that.
12 | # Added (renamed)on 2025-04-07
13 | ignored_paths: []
14 | 
15 | # whether the project is in read-only mode
16 | # If set to true, all editing tools will be disabled and attempts to use them will result in an error
17 | # Added on 2025-04-18
18 | read_only: false
19 | 
20 | 
21 | # list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
22 | # Below is the complete list of tools for convenience.
23 | # To make sure you have the latest list of tools, and to view their descriptions, 
24 | # execute `uv run scripts/print_tool_overview.py`.
25 | #
26 | #  * `activate_project`: Activates a project by name.
27 | #  * `check_onboarding_performed`: Checks whether project onboarding was already performed.
28 | #  * `create_text_file`: Creates/overwrites a file in the project directory.
29 | #  * `delete_lines`: Deletes a range of lines within a file.
30 | #  * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
31 | #  * `execute_shell_command`: Executes a shell command.
32 | #  * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
33 | #  * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
34 | #  * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
35 | #  * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
36 | #  * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
37 | #  * `initial_instructions`: Gets the initial instructions for the current project.
38 | #     Should only be used in settings where the system prompt cannot be set,
39 | #     e.g. in clients you have no control over, like Claude Desktop.
40 | #  * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
41 | #  * `insert_at_line`: Inserts content at a given line in a file.
42 | #  * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
43 | #  * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
44 | #  * `list_memories`: Lists memories in Serena's project-specific memory store.
45 | #  * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
46 | #  * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
47 | #  * `read_file`: Reads a file within the project directory.
48 | #  * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
49 | #  * `remove_project`: Removes a project from the Serena configuration.
50 | #  * `replace_lines`: Replaces a range of lines within a file with new content.
51 | #  * `replace_symbol_body`: Replaces the full definition of a symbol.
52 | #  * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
53 | #  * `search_for_pattern`: Performs a search for a pattern in the project.
54 | #  * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
55 | #  * `switch_modes`: Activates modes by providing a list of their names
56 | #  * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
57 | #  * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
58 | #  * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
59 | #  * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
60 | excluded_tools: []
61 | 
62 | # initial prompt for the project. It will always be given to the LLM upon activating the project
63 | # (contrary to the memories, which are loaded on demand).
64 | initial_prompt: ""
65 | 
66 | project_name: "serena"
67 | 
```

--------------------------------------------------------------------------------
/src/serena/resources/project.template.yml:
--------------------------------------------------------------------------------

```yaml
 1 | # language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
 2 | #  * For C, use cpp
 3 | #  * For JavaScript, use typescript
 4 | # Special requirements:
 5 | #  * csharp: Requires the presence of a .sln file in the project folder.
 6 | language: python
 7 | 
 8 | # whether to use the project's gitignore file to ignore files
 9 | # Added on 2025-04-07
10 | ignore_all_files_in_gitignore: true
11 | # list of additional paths to ignore
12 | # same syntax as gitignore, so you can use * and **
13 | # Was previously called `ignored_dirs`, please update your config if you are using that.
14 | # Added (renamed) on 2025-04-07
15 | ignored_paths: []
16 | 
17 | # whether the project is in read-only mode
18 | # If set to true, all editing tools will be disabled and attempts to use them will result in an error
19 | # Added on 2025-04-18
20 | read_only: false
21 | 
22 | # list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
23 | # Below is the complete list of tools for convenience.
24 | # To make sure you have the latest list of tools, and to view their descriptions, 
25 | # execute `uv run scripts/print_tool_overview.py`.
26 | #
27 | #  * `activate_project`: Activates a project by name.
28 | #  * `check_onboarding_performed`: Checks whether project onboarding was already performed.
29 | #  * `create_text_file`: Creates/overwrites a file in the project directory.
30 | #  * `delete_lines`: Deletes a range of lines within a file.
31 | #  * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
32 | #  * `execute_shell_command`: Executes a shell command.
33 | #  * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
34 | #  * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
35 | #  * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
36 | #  * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
37 | #  * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
38 | #  * `initial_instructions`: Gets the initial instructions for the current project.
39 | #     Should only be used in settings where the system prompt cannot be set,
40 | #     e.g. in clients you have no control over, like Claude Desktop.
41 | #  * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
42 | #  * `insert_at_line`: Inserts content at a given line in a file.
43 | #  * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
44 | #  * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
45 | #  * `list_memories`: Lists memories in Serena's project-specific memory store.
46 | #  * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
47 | #  * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
48 | #  * `read_file`: Reads a file within the project directory.
49 | #  * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
50 | #  * `remove_project`: Removes a project from the Serena configuration.
51 | #  * `replace_lines`: Replaces a range of lines within a file with new content.
52 | #  * `replace_symbol_body`: Replaces the full definition of a symbol.
53 | #  * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
54 | #  * `search_for_pattern`: Performs a search for a pattern in the project.
55 | #  * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
56 | #  * `switch_modes`: Activates modes by providing a list of their names
57 | #  * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
58 | #  * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
59 | #  * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
60 | #  * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
61 | excluded_tools: []
62 | 
63 | # initial prompt for the project. It will always be given to the LLM upon activating the project
64 | # (contrary to the memories, which are loaded on demand).
65 | initial_prompt: ""
66 | 
67 | project_name: "project_name"
68 | 
```

--------------------------------------------------------------------------------
/lessons_learned.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Lessons Learned
 2 | 
 3 | In this document we briefly collect what we have learned while developing and using Serena,
 4 | what works well and what doesn't.
 5 | 
 6 | ## What Worked
 7 | 
 8 | ### Separate Tool Logic From MCP Implementation
 9 | 
10 | MCP is just another protocol, one should let the details of it creep into the application logic.
11 | The official docs suggest using function annotations to define tools and prompts. While that may be
12 | useful for small projects to get going fast, it is not wise for more serious projects. In Serena,
13 | all tools are defined independently and then converted to instances of `MCPTool` using our `make_tool`
14 | function.
15 | 
16 | ### Autogenerated PromptFactory
17 | 
18 | Prompt templates are central for most LLM applications, so one needs good representations of them in the code,
19 | while at the same time they often need to be customizable and exposed to users. In Serena we address these conflicting 
20 | needs by defining prompt templates (in jinja format) in separate yamls that users can easily modify and by autogenerated
21 | a `PromptFactory` class with meaningful method and parameter names from these yamls. The latter is committed to our code.
22 | We separated out the generation logic into the [interprompt](/src/interprompt/README.md) subpackage that can be used as a library.
23 | 
24 | ### Tempfiles and Snapshots for Testing of Editing Tools
25 | 
26 | We test most aspects of Serena by having a small "project" for each supported language in `tests/resources`.
27 | For the editing tools, which would change the code in these projects, we use tempfiles to copy over the code.
28 | The pretty awesome [syrupy](https://github.com/syrupy-project/syrupy) pytest plugin helped in developing
29 | snapshot tests.
30 | 
31 | ### Dashboard and GUI for Logging
32 | 
33 | It is very useful to know what the MCP Server is doing. We collect and display logs in a GUI or a web dashboard,
34 | which helps a lot in seeing what's going on and in identifying any issues.
35 | 
36 | ### Unrestricted Bash Tool
37 | 
38 | We know it's not particularly safe to permit unlimited shell commands outside a sandbox, but we did quite some
39 | evaluations and so far... nothing bad has happened. Seems like the current versions of the AI overlords rarely want to execute `sudo rm - rf /`.
40 | Still, we are working on a safer approach as well as better integration with sandboxing.
41 | 
42 | ### Multilspy
43 | 
44 | The [multilspy](https://github.com/microsoft/multilspy/) project helped us a lot in getting started and stands at the core of Serena.
45 | Many more well known python implementations of language servers were subpar in code quality and design (for example, missing types).
46 | 
47 | ### Developing Serena with Serena
48 | 
49 | We clearly notice that the better the tool gets, the easier it is to make it even better
50 | 
51 | ## Prompting
52 | 
53 | ### Shouting and Emotive Language May Be Needed
54 | 
55 | When developing the `ReplaceRegexTool` we were initially not able to make Claude 4 (in Claude Desktop) use wildcards to save on output tokens. Neither
56 | examples nor explicit instructions helped. It was only after adding 
57 | 
58 | ```
59 | IMPORTANT: REMEMBER TO USE WILDCARDS WHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE LONG REGEXES WITHOUT USING WILDCARDS INSTEAD!
60 | ```
61 | 
62 | to the initial instructions and to the tool description that Claude finally started following the instructions.
63 | 
64 | ## What Didn't Work
65 | 
66 | ### Lifespan Handling by MCP Clients
67 | 
68 | The MCP technology is clearly very green. Even though there is a lifespan context in the MCP SDK,
69 | many clients, including Claude Desktop, fail to properly clean up, leaving zombie processes behind.
70 | We mitigate this through the GUI window and the dashboard, so the user sees whether Serena is running
71 | and can terminate it there.
72 | 
73 | ### Trusting Asyncio
74 | 
75 | Running multiple asyncio apps led to non-deterministic 
76 | event loop contamination and deadlocks, which were very hard to debug
77 | and understand. We solved this with a large hammer, by putting all asyncio apps into a separate
78 | process. It made the code much more complex and slightly enhanced RAM requirements, but it seems
79 | like that was the only way to reliably overcome asyncio deadlock issues.
80 | 
81 | ### Cross-OS Tkinter GUI
82 | 
83 | Different OS have different limitations when it comes to starting a window or dealing with Tkinter
84 | installations. This was so messy to get right that we pivoted to a web-dashboard instead
85 | 
86 | ### Editing Based on Line Numbers
87 | 
88 | Not only are LLMs notoriously bad in counting, but also the line numbers change after edit operations,
89 | and LLMs are also often too dumb to understand that they should update the line numbers information they had
90 | received before. We pivoted to string-matching and symbol-name based editing.
```

--------------------------------------------------------------------------------
/test/conftest.py:
--------------------------------------------------------------------------------

```python
  1 | import logging
  2 | from pathlib import Path
  3 | 
  4 | import pytest
  5 | from sensai.util.logging import configure
  6 | 
  7 | from serena.constants import SERENA_MANAGED_DIR_IN_HOME, SERENA_MANAGED_DIR_NAME
  8 | from serena.project import Project
  9 | from serena.util.file_system import GitignoreParser
 10 | from solidlsp.ls import SolidLanguageServer
 11 | from solidlsp.ls_config import Language, LanguageServerConfig
 12 | from solidlsp.ls_logger import LanguageServerLogger
 13 | from solidlsp.settings import SolidLSPSettings
 14 | 
 15 | configure(level=logging.ERROR)
 16 | 
 17 | 
 18 | @pytest.fixture(scope="session")
 19 | def resources_dir() -> Path:
 20 |     """Path to the test resources directory."""
 21 |     current_dir = Path(__file__).parent
 22 |     return current_dir / "resources"
 23 | 
 24 | 
 25 | class LanguageParamRequest:
 26 |     param: Language
 27 | 
 28 | 
 29 | def get_repo_path(language: Language) -> Path:
 30 |     return Path(__file__).parent / "resources" / "repos" / language / "test_repo"
 31 | 
 32 | 
 33 | def create_ls(
 34 |     language: Language,
 35 |     repo_path: str | None = None,
 36 |     ignored_paths: list[str] | None = None,
 37 |     trace_lsp_communication: bool = False,
 38 |     log_level: int = logging.ERROR,
 39 | ) -> SolidLanguageServer:
 40 |     ignored_paths = ignored_paths or []
 41 |     if repo_path is None:
 42 |         repo_path = str(get_repo_path(language))
 43 |     gitignore_parser = GitignoreParser(str(repo_path))
 44 |     for spec in gitignore_parser.get_ignore_specs():
 45 |         ignored_paths.extend(spec.patterns)
 46 |     config = LanguageServerConfig(code_language=language, ignored_paths=ignored_paths, trace_lsp_communication=trace_lsp_communication)
 47 |     logger = LanguageServerLogger(log_level=log_level)
 48 |     return SolidLanguageServer.create(
 49 |         config,
 50 |         logger,
 51 |         repo_path,
 52 |         solidlsp_settings=SolidLSPSettings(solidlsp_dir=SERENA_MANAGED_DIR_IN_HOME, project_data_relative_path=SERENA_MANAGED_DIR_NAME),
 53 |     )
 54 | 
 55 | 
 56 | def create_default_ls(language: Language) -> SolidLanguageServer:
 57 |     repo_path = str(get_repo_path(language))
 58 |     return create_ls(language, repo_path)
 59 | 
 60 | 
 61 | def create_default_project(language: Language) -> Project:
 62 |     repo_path = str(get_repo_path(language))
 63 |     return Project.load(repo_path)
 64 | 
 65 | 
 66 | @pytest.fixture(scope="session")
 67 | def repo_path(request: LanguageParamRequest) -> Path:
 68 |     """Get the repository path for a specific language.
 69 | 
 70 |     This fixture requires a language parameter via pytest.mark.parametrize:
 71 | 
 72 |     Example:
 73 |     ```
 74 |     @pytest.mark.parametrize("repo_path", [Language.PYTHON], indirect=True)
 75 |     def test_python_repo(repo_path):
 76 |         assert (repo_path / "src").exists()
 77 |     ```
 78 | 
 79 |     """
 80 |     if not hasattr(request, "param"):
 81 |         raise ValueError("Language parameter must be provided via pytest.mark.parametrize")
 82 | 
 83 |     language = request.param
 84 |     return get_repo_path(language)
 85 | 
 86 | 
 87 | @pytest.fixture(scope="session")
 88 | def language_server(request: LanguageParamRequest):
 89 |     """Create a language server instance configured for the specified language.
 90 | 
 91 |     This fixture requires a language parameter via pytest.mark.parametrize:
 92 | 
 93 |     Example:
 94 |     ```
 95 |     @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
 96 |     def test_python_server(language_server: SyncLanguageServer) -> None:
 97 |         # Use the Python language server
 98 |         pass
 99 |     ```
100 | 
101 |     You can also test multiple languages in a single test:
102 |     ```
103 |     @pytest.mark.parametrize("language_server", [Language.PYTHON, Language.TYPESCRIPT], indirect=True)
104 |     def test_multiple_languages(language_server: SyncLanguageServer) -> None:
105 |         # This test will run once for each language
106 |         pass
107 |     ```
108 | 
109 |     """
110 |     if not hasattr(request, "param"):
111 |         raise ValueError("Language parameter must be provided via pytest.mark.parametrize")
112 | 
113 |     language = request.param
114 |     server = create_default_ls(language)
115 |     server.start()
116 |     try:
117 |         yield server
118 |     finally:
119 |         server.stop()
120 | 
121 | 
122 | @pytest.fixture(scope="session")
123 | def project(request: LanguageParamRequest):
124 |     """Create a Project for the specified language.
125 | 
126 |     This fixture requires a language parameter via pytest.mark.parametrize:
127 | 
128 |     Example:
129 |     ```
130 |     @pytest.mark.parametrize("project", [Language.PYTHON], indirect=True)
131 |     def test_python_project(project: Project) -> None:
132 |         # Use the Python project to test something
133 |         pass
134 |     ```
135 | 
136 |     You can also test multiple languages in a single test:
137 |     ```
138 |     @pytest.mark.parametrize("project", [Language.PYTHON, Language.TYPESCRIPT], indirect=True)
139 |     def test_multiple_languages(project: SyncLanguageServer) -> None:
140 |         # This test will run once for each language
141 |         pass
142 |     ```
143 | 
144 |     """
145 |     if not hasattr(request, "param"):
146 |         raise ValueError("Language parameter must be provided via pytest.mark.parametrize")
147 | 
148 |     language = request.param
149 |     yield create_default_project(language)
150 | 
```

--------------------------------------------------------------------------------
/src/serena/resources/serena_config.template.yml:
--------------------------------------------------------------------------------

```yaml
 1 | gui_log_window: False
 2 | # whether to open a graphical window with Serena's logs.
 3 | # This is mainly supported on Windows and (partly) on Linux; not available on macOS.
 4 | # If you want to see the logs in a web browser, use the `web_dashboard` option instead.
 5 | # Limitations: doesn't seem to work with the community version of Claude Desktop for Linux
 6 | # Might also cause problems with some MCP clients - if you have any issues, try disabling this
 7 | 
 8 | # Being able to inspect logs is useful both for troubleshooting and for monitoring the tool calls,
 9 | # especially when using the agno playground, since the tool calls are not always shown,
10 | # and the input params are never shown in the agno UI.
11 | # When used as MCP server for Claude Desktop, the logs are primarily for troubleshooting.
12 | # Note: unfortunately, the various entities starting the Serena server or agent do so in
13 | # mysterious ways, often starting multiple instances of the process without shutting down
14 | # previous instances. This can lead to multiple log windows being opened, and only the last
15 | # window being updated. Since we can't control how agno or Claude Desktop start Serena,
16 | # we have to live with this limitation for now.
17 | 
18 | web_dashboard: True
19 | # whether to open the Serena web dashboard (which will be accessible through your web browser) that
20 | # shows Serena's current session logs - as an alternative to the GUI log window which
21 | # is supported on all platforms.
22 | 
23 | web_dashboard_open_on_launch: True
24 | # whether to open a browser window with the web dashboard when Serena starts (provided that web_dashboard
25 | # is enabled). If set to False, you can still open the dashboard manually by navigating to
26 | # http://localhost:24282/dashboard/ in your web browser (24282 = 0x5EDA, SErena DAshboard).
27 | # If you have multiple instances running, a higher port will be used; try port 24283, 24284, etc.
28 | 
29 | log_level: 20
30 | # the minimum log level for the GUI log window and the dashboard (10 = debug, 20 = info, 30 = warning, 40 = error)
31 | 
32 | trace_lsp_communication: False
33 | # whether to trace the communication between Serena and the language servers.
34 | # This is useful for debugging language server issues.
35 | 
36 | ls_specific_settings: {}
37 | # Added on 23.08.2025
38 | # Advanced configuration option allowing to configure language server implementation specific options. Maps the language
39 | # (same entry as in project.yml) to the options.
40 | # Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
41 | # No documentation on options means no options are available.
42 | #
43 | 
44 | tool_timeout: 240
45 | # timeout, in seconds, after which tool executions are terminated
46 | 
47 | excluded_tools: []
48 | # list of tools to be globally excluded
49 | 
50 | included_optional_tools: []
51 | # list of optional tools (which are disabled by default) to be included
52 | 
53 | jetbrains: False
54 | # whether to enable JetBrains mode and use tools based on the Serena JetBrains IDE plugin
55 | # instead of language server-based tools
56 | # NOTE: The plugin is yet unreleased. This is for Serena developers only.
57 | 
58 | 
59 | default_max_tool_answer_chars: 150000
60 | # Used as default for tools where the apply method has a default maximal answer length.
61 | # Even though the value of the max_answer_chars can be changed when calling the tool, it may make sense to adjust this default
62 | # through the global configuration.
63 | 
64 | record_tool_usage_stats:  False
65 | # whether to record tool usage statistics, they will be shown in the web dashboard if recording is active.
66 | 
67 | token_count_estimator: TIKTOKEN_GPT4O
68 | # Only relevant if `record_tool_usage` is True; the name of the token count estimator to use for tool usage statistics.
69 | # See the `RegisteredTokenCountEstimator` enum for available options.
70 | #
71 | # Note: some token estimators (like tiktoken) may require downloading data files
72 | # on the first run, which can take some time and require internet access. Others, like the Anthropic ones, may require an API key
73 | # and rate limits may apply.
74 | 
75 | 
76 | # MANAGED BY SERENA, KEEP AT THE BOTTOM OF THE YAML AND DON'T EDIT WITHOUT NEED
77 | # The list of registered projects.
78 | # To add a project, within a chat, simply ask Serena to "activate the project /path/to/project" or,
79 | # if the project was previously added, "activate the project <project name>".
80 | # By default, the project's name will be the name of the directory containing the project, but you may change it
81 | # by editing the (auto-generated) project configuration file `/path/project/project/.serena/project.yml` file.
82 | # If you want to maintain full control of the project configuration, create the project.yml file manually and then
83 | # instruct Serena to activate the project by its path for first-time activation.
84 | # NOTE: Make sure there are no name collisions in the names of registered projects.
85 | projects: []
86 | 
```

--------------------------------------------------------------------------------
/test/resources/repos/python/test_repo/examples/user_management.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Example demonstrating user management with the test_repo module.
  3 | 
  4 | This example showcases:
  5 | - Creating and managing users
  6 | - Using various object types and relationships
  7 | - Type annotations and complex Python patterns
  8 | """
  9 | 
 10 | import logging
 11 | from dataclasses import dataclass
 12 | from typing import Any
 13 | 
 14 | from test_repo.models import User, create_user_object
 15 | from test_repo.services import UserService
 16 | 
 17 | # Set up logging
 18 | logging.basicConfig(level=logging.INFO)
 19 | logger = logging.getLogger(__name__)
 20 | 
 21 | 
 22 | @dataclass
 23 | class UserStats:
 24 |     """Statistics about user activity."""
 25 | 
 26 |     user_id: str
 27 |     login_count: int = 0
 28 |     last_active_days: int = 0
 29 |     engagement_score: float = 0.0
 30 | 
 31 |     def is_active(self) -> bool:
 32 |         """Check if the user is considered active."""
 33 |         return self.last_active_days < 30
 34 | 
 35 | 
 36 | class UserManager:
 37 |     """Example class demonstrating complex user management."""
 38 | 
 39 |     def __init__(self, service: UserService):
 40 |         self.service = service
 41 |         self.active_users: dict[str, User] = {}
 42 |         self.user_stats: dict[str, UserStats] = {}
 43 | 
 44 |     def register_user(self, name: str, email: str, roles: list[str] | None = None) -> User:
 45 |         """Register a new user."""
 46 |         logger.info(f"Registering new user: {name} ({email})")
 47 |         user = self.service.create_user(name=name, email=email, roles=roles)
 48 |         self.active_users[user.id] = user
 49 |         self.user_stats[user.id] = UserStats(user_id=user.id)
 50 |         return user
 51 | 
 52 |     def get_user(self, user_id: str) -> User | None:
 53 |         """Get a user by ID."""
 54 |         if user_id in self.active_users:
 55 |             return self.active_users[user_id]
 56 | 
 57 |         # Try to fetch from service
 58 |         user = self.service.get_user(user_id)
 59 |         if user:
 60 |             self.active_users[user.id] = user
 61 |         return user
 62 | 
 63 |     def update_user_stats(self, user_id: str, login_count: int, days_since_active: int) -> None:
 64 |         """Update statistics for a user."""
 65 |         if user_id not in self.user_stats:
 66 |             self.user_stats[user_id] = UserStats(user_id=user_id)
 67 | 
 68 |         stats = self.user_stats[user_id]
 69 |         stats.login_count = login_count
 70 |         stats.last_active_days = days_since_active
 71 | 
 72 |         # Calculate engagement score based on activity
 73 |         engagement = (100 - min(days_since_active, 100)) * 0.8
 74 |         engagement += min(login_count, 20) * 0.2
 75 |         stats.engagement_score = engagement
 76 | 
 77 |     def get_active_users(self) -> list[User]:
 78 |         """Get all active users."""
 79 |         active_user_ids = [user_id for user_id, stats in self.user_stats.items() if stats.is_active()]
 80 |         return [self.active_users[user_id] for user_id in active_user_ids if user_id in self.active_users]
 81 | 
 82 |     def get_user_by_email(self, email: str) -> User | None:
 83 |         """Find a user by their email address."""
 84 |         for user in self.active_users.values():
 85 |             if user.email == email:
 86 |                 return user
 87 |         return None
 88 | 
 89 | 
 90 | # Example function demonstrating type annotations
 91 | def process_user_data(users: list[User], include_inactive: bool = False, transform_func: callable | None = None) -> dict[str, Any]:
 92 |     """Process user data with optional transformations."""
 93 |     result: dict[str, Any] = {"users": [], "total": 0, "admin_count": 0}
 94 | 
 95 |     for user in users:
 96 |         if transform_func:
 97 |             user_data = transform_func(user.to_dict())
 98 |         else:
 99 |             user_data = user.to_dict()
100 | 
101 |         result["users"].append(user_data)
102 |         result["total"] += 1
103 | 
104 |         if "admin" in user.roles:
105 |             result["admin_count"] += 1
106 | 
107 |     return result
108 | 
109 | 
110 | def main():
111 |     """Main function demonstrating the usage of UserManager."""
112 |     # Initialize service and manager
113 |     service = UserService()
114 |     manager = UserManager(service)
115 | 
116 |     # Register some users
117 |     admin = manager.register_user("Admin User", "[email protected]", ["admin"])
118 |     user1 = manager.register_user("Regular User", "[email protected]", ["user"])
119 |     user2 = manager.register_user("Another User", "[email protected]", ["user"])
120 | 
121 |     # Update some stats
122 |     manager.update_user_stats(admin.id, 100, 5)
123 |     manager.update_user_stats(user1.id, 50, 10)
124 |     manager.update_user_stats(user2.id, 10, 45)  # Inactive user
125 | 
126 |     # Get active users
127 |     active_users = manager.get_active_users()
128 |     logger.info(f"Active users: {len(active_users)}")
129 | 
130 |     # Process user data
131 |     user_data = process_user_data(active_users, transform_func=lambda u: {**u, "full_name": u.get("name", "")})
132 | 
133 |     logger.info(f"Processed {user_data['total']} users, {user_data['admin_count']} admins")
134 | 
135 |     # Example of calling create_user directly
136 |     external_user = create_user_object(id="ext123", name="External User", email="[email protected]", roles=["external"])
137 |     logger.info(f"Created external user: {external_user.name}")
138 | 
139 | 
140 | if __name__ == "__main__":
141 |     main()
142 | 
```

--------------------------------------------------------------------------------
/test/resources/repos/python/test_repo/ignore_this_dir_with_postfix/ignored_module.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Example demonstrating user management with the test_repo module.
  3 | 
  4 | This example showcases:
  5 | - Creating and managing users
  6 | - Using various object types and relationships
  7 | - Type annotations and complex Python patterns
  8 | """
  9 | 
 10 | import logging
 11 | from dataclasses import dataclass
 12 | from typing import Any
 13 | 
 14 | from test_repo.models import User, create_user_object
 15 | from test_repo.services import UserService
 16 | 
 17 | # Set up logging
 18 | logging.basicConfig(level=logging.INFO)
 19 | logger = logging.getLogger(__name__)
 20 | 
 21 | 
 22 | @dataclass
 23 | class UserStats:
 24 |     """Statistics about user activity."""
 25 | 
 26 |     user_id: str
 27 |     login_count: int = 0
 28 |     last_active_days: int = 0
 29 |     engagement_score: float = 0.0
 30 | 
 31 |     def is_active(self) -> bool:
 32 |         """Check if the user is considered active."""
 33 |         return self.last_active_days < 30
 34 | 
 35 | 
 36 | class UserManager:
 37 |     """Example class demonstrating complex user management."""
 38 | 
 39 |     def __init__(self, service: UserService):
 40 |         self.service = service
 41 |         self.active_users: dict[str, User] = {}
 42 |         self.user_stats: dict[str, UserStats] = {}
 43 | 
 44 |     def register_user(self, name: str, email: str, roles: list[str] | None = None) -> User:
 45 |         """Register a new user."""
 46 |         logger.info(f"Registering new user: {name} ({email})")
 47 |         user = self.service.create_user(name=name, email=email, roles=roles)
 48 |         self.active_users[user.id] = user
 49 |         self.user_stats[user.id] = UserStats(user_id=user.id)
 50 |         return user
 51 | 
 52 |     def get_user(self, user_id: str) -> User | None:
 53 |         """Get a user by ID."""
 54 |         if user_id in self.active_users:
 55 |             return self.active_users[user_id]
 56 | 
 57 |         # Try to fetch from service
 58 |         user = self.service.get_user(user_id)
 59 |         if user:
 60 |             self.active_users[user.id] = user
 61 |         return user
 62 | 
 63 |     def update_user_stats(self, user_id: str, login_count: int, days_since_active: int) -> None:
 64 |         """Update statistics for a user."""
 65 |         if user_id not in self.user_stats:
 66 |             self.user_stats[user_id] = UserStats(user_id=user_id)
 67 | 
 68 |         stats = self.user_stats[user_id]
 69 |         stats.login_count = login_count
 70 |         stats.last_active_days = days_since_active
 71 | 
 72 |         # Calculate engagement score based on activity
 73 |         engagement = (100 - min(days_since_active, 100)) * 0.8
 74 |         engagement += min(login_count, 20) * 0.2
 75 |         stats.engagement_score = engagement
 76 | 
 77 |     def get_active_users(self) -> list[User]:
 78 |         """Get all active users."""
 79 |         active_user_ids = [user_id for user_id, stats in self.user_stats.items() if stats.is_active()]
 80 |         return [self.active_users[user_id] for user_id in active_user_ids if user_id in self.active_users]
 81 | 
 82 |     def get_user_by_email(self, email: str) -> User | None:
 83 |         """Find a user by their email address."""
 84 |         for user in self.active_users.values():
 85 |             if user.email == email:
 86 |                 return user
 87 |         return None
 88 | 
 89 | 
 90 | # Example function demonstrating type annotations
 91 | def process_user_data(users: list[User], include_inactive: bool = False, transform_func: callable | None = None) -> dict[str, Any]:
 92 |     """Process user data with optional transformations."""
 93 |     result: dict[str, Any] = {"users": [], "total": 0, "admin_count": 0}
 94 | 
 95 |     for user in users:
 96 |         if transform_func:
 97 |             user_data = transform_func(user.to_dict())
 98 |         else:
 99 |             user_data = user.to_dict()
100 | 
101 |         result["users"].append(user_data)
102 |         result["total"] += 1
103 | 
104 |         if "admin" in user.roles:
105 |             result["admin_count"] += 1
106 | 
107 |     return result
108 | 
109 | 
110 | def main():
111 |     """Main function demonstrating the usage of UserManager."""
112 |     # Initialize service and manager
113 |     service = UserService()
114 |     manager = UserManager(service)
115 | 
116 |     # Register some users
117 |     admin = manager.register_user("Admin User", "[email protected]", ["admin"])
118 |     user1 = manager.register_user("Regular User", "[email protected]", ["user"])
119 |     user2 = manager.register_user("Another User", "[email protected]", ["user"])
120 | 
121 |     # Update some stats
122 |     manager.update_user_stats(admin.id, 100, 5)
123 |     manager.update_user_stats(user1.id, 50, 10)
124 |     manager.update_user_stats(user2.id, 10, 45)  # Inactive user
125 | 
126 |     # Get active users
127 |     active_users = manager.get_active_users()
128 |     logger.info(f"Active users: {len(active_users)}")
129 | 
130 |     # Process user data
131 |     user_data = process_user_data(active_users, transform_func=lambda u: {**u, "full_name": u.get("name", "")})
132 | 
133 |     logger.info(f"Processed {user_data['total']} users, {user_data['admin_count']} admins")
134 | 
135 |     # Example of calling create_user directly
136 |     external_user = create_user_object(id="ext123", name="External User", email="[email protected]", roles=["external"])
137 |     logger.info(f"Created external user: {external_user.name}")
138 | 
139 | 
140 | if __name__ == "__main__":
141 |     main()
142 | 
```
Page 2/14FirstPrevNextLast