monoco-toolkit 0.2.2__py3-none-any.whl → 0.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- monoco/cli/__init__.py +0 -0
- monoco/cli/project.py +79 -0
- monoco/cli/workspace.py +38 -0
- monoco/core/agent/state.py +32 -43
- monoco/core/config.py +46 -20
- monoco/core/integrations.py +53 -0
- monoco/core/lsp.py +61 -0
- monoco/core/setup.py +87 -124
- monoco/core/workspace.py +15 -9
- monoco/features/issue/commands.py +48 -1
- monoco/features/issue/core.py +78 -14
- monoco/features/issue/linter.py +215 -116
- monoco/features/issue/models.py +2 -15
- monoco/features/issue/resources/en/AGENTS.md +7 -1
- monoco/features/issue/resources/en/SKILL.md +39 -3
- monoco/features/issue/resources/zh/AGENTS.md +8 -2
- monoco/features/issue/resources/zh/SKILL.md +32 -3
- monoco/features/issue/validator.py +246 -0
- monoco/main.py +54 -4
- {monoco_toolkit-0.2.2.dist-info → monoco_toolkit-0.2.3.dist-info}/METADATA +1 -1
- {monoco_toolkit-0.2.2.dist-info → monoco_toolkit-0.2.3.dist-info}/RECORD +24 -19
- {monoco_toolkit-0.2.2.dist-info → monoco_toolkit-0.2.3.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.2.2.dist-info → monoco_toolkit-0.2.3.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.2.2.dist-info → monoco_toolkit-0.2.3.dist-info}/licenses/LICENSE +0 -0
monoco/cli/__init__.py
ADDED
|
File without changes
|
monoco/cli/project.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
import yaml
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from monoco.core.workspace import find_projects, is_project_root
|
|
11
|
+
from monoco.core.config import get_config
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="Manage Monoco Projects")
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
@app.command("list")
|
|
17
|
+
def list_projects(
|
|
18
|
+
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
19
|
+
root: Optional[str] = typer.Option(None, "--root", help="Workspace root")
|
|
20
|
+
):
|
|
21
|
+
"""List all discovered projects in the workspace."""
|
|
22
|
+
cwd = Path(root).resolve() if root else Path.cwd()
|
|
23
|
+
projects = find_projects(cwd)
|
|
24
|
+
|
|
25
|
+
if json_output:
|
|
26
|
+
data = [
|
|
27
|
+
{
|
|
28
|
+
"id": p.id,
|
|
29
|
+
"name": p.name,
|
|
30
|
+
"path": str(p.path),
|
|
31
|
+
"key": p.config.project.key if p.config.project else ""
|
|
32
|
+
}
|
|
33
|
+
for p in projects
|
|
34
|
+
]
|
|
35
|
+
print(json.dumps(data))
|
|
36
|
+
else:
|
|
37
|
+
table = Table(title=f"Projects in {cwd}")
|
|
38
|
+
table.add_column("ID", style="cyan")
|
|
39
|
+
table.add_column("Name", style="magenta")
|
|
40
|
+
table.add_column("Key", style="green")
|
|
41
|
+
table.add_column("Path", style="dim")
|
|
42
|
+
|
|
43
|
+
for p in projects:
|
|
44
|
+
path_str = str(p.path.relative_to(cwd)) if p.path.is_relative_to(cwd) else str(p.path)
|
|
45
|
+
if path_str == ".":
|
|
46
|
+
path_str = "(root)"
|
|
47
|
+
key = p.config.project.key if p.config.project else "N/A"
|
|
48
|
+
table.add_row(p.id, p.name, key, path_str)
|
|
49
|
+
|
|
50
|
+
console.print(table)
|
|
51
|
+
|
|
52
|
+
@app.command("init")
|
|
53
|
+
def init_project(
|
|
54
|
+
name: str = typer.Option(..., "--name", "-n", help="Project Name"),
|
|
55
|
+
key: str = typer.Option(..., "--key", "-k", help="Project Key"),
|
|
56
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config")
|
|
57
|
+
):
|
|
58
|
+
"""Initialize a new project in the current directory."""
|
|
59
|
+
cwd = Path.cwd()
|
|
60
|
+
project_config_path = cwd / ".monoco" / "project.yaml"
|
|
61
|
+
|
|
62
|
+
if project_config_path.exists() and not force:
|
|
63
|
+
console.print(f"[yellow]Project already initialized in {cwd}. Use --force to overwrite.[/yellow]")
|
|
64
|
+
raise typer.Exit(code=1)
|
|
65
|
+
|
|
66
|
+
cwd.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
(cwd / ".monoco").mkdir(exist_ok=True)
|
|
68
|
+
|
|
69
|
+
config = {
|
|
70
|
+
"project": {
|
|
71
|
+
"name": name,
|
|
72
|
+
"key": key
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
with open(project_config_path, "w") as f:
|
|
77
|
+
yaml.dump(config, f, default_flow_style=False)
|
|
78
|
+
|
|
79
|
+
console.print(f"[green]Initialized project '{name}' ({key}) in {cwd}[/green]")
|
monoco/cli/workspace.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
import yaml
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
app = typer.Typer(help="Manage Monoco Workspace")
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
@app.command("init")
|
|
11
|
+
def init_workspace(
|
|
12
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config")
|
|
13
|
+
):
|
|
14
|
+
"""Initialize a workspace environment in the current directory."""
|
|
15
|
+
cwd = Path.cwd()
|
|
16
|
+
workspace_config_path = cwd / ".monoco" / "workspace.yaml"
|
|
17
|
+
|
|
18
|
+
if workspace_config_path.exists() and not force:
|
|
19
|
+
console.print(f"[yellow]Workspace already initialized in {cwd}. Use --force to overwrite.[/yellow]")
|
|
20
|
+
raise typer.Exit(code=1)
|
|
21
|
+
|
|
22
|
+
cwd.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
(cwd / ".monoco").mkdir(exist_ok=True)
|
|
24
|
+
|
|
25
|
+
# Default workspace config
|
|
26
|
+
config = {
|
|
27
|
+
"paths": {
|
|
28
|
+
"issues": "Issues", # Default
|
|
29
|
+
"spikes": ".references",
|
|
30
|
+
"specs": "SPECS"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
with open(workspace_config_path, "w") as f:
|
|
35
|
+
yaml.dump(config, f, default_flow_style=False)
|
|
36
|
+
|
|
37
|
+
console.print(f"[green]Initialized workspace in {cwd}[/green]")
|
|
38
|
+
|
monoco/core/agent/state.py
CHANGED
|
@@ -62,56 +62,45 @@ class AgentStateManager:
|
|
|
62
62
|
return self.refresh()
|
|
63
63
|
|
|
64
64
|
def refresh(self) -> AgentState:
|
|
65
|
-
"""Run
|
|
65
|
+
"""Run diagnostics on all integrations and update state."""
|
|
66
66
|
logger.info("Refreshing agent state...")
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# For dev environment: Toolkit/scripts/check_agents.sh
|
|
71
|
-
# For production: It might need to be packaged or generated.
|
|
68
|
+
from monoco.core.integrations import get_all_integrations
|
|
69
|
+
from monoco.core.config import get_config
|
|
72
70
|
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
71
|
+
# Load config to get possible overrides
|
|
72
|
+
# Determine root (hacky for now, should be passed)
|
|
73
|
+
root = Path.cwd()
|
|
74
|
+
config = get_config(str(root))
|
|
75
|
+
|
|
76
|
+
integrations = get_all_integrations(config_overrides=config.agent.integrations, enabled_only=True)
|
|
77
|
+
|
|
78
|
+
providers = {}
|
|
79
|
+
for key, integration in integrations.items():
|
|
80
|
+
if not integration.bin_name:
|
|
81
|
+
continue # Skip integrations that don't have a binary component
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
health = integration.check_health()
|
|
84
|
+
providers[key] = AgentProviderState(
|
|
85
|
+
available=health.available,
|
|
86
|
+
path=health.path,
|
|
87
|
+
error=health.error,
|
|
88
|
+
latency_ms=health.latency_ms
|
|
87
89
|
)
|
|
88
90
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
state = AgentState(
|
|
92
|
+
last_checked=datetime.now(timezone.utc),
|
|
93
|
+
providers=providers
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Save state
|
|
97
|
+
self.state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
with open(self.state_path, "w") as f:
|
|
99
|
+
yaml.dump(state.model_dump(mode='json'), f)
|
|
96
100
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
raise RuntimeError(f"Agent check failed: {e.stderr}") from e
|
|
101
|
+
self._state = state
|
|
102
|
+
return state
|
|
100
103
|
|
|
101
104
|
def _find_script(self) -> Optional[Path]:
|
|
102
|
-
"""
|
|
103
|
-
# Check dev path relative to this file
|
|
104
|
-
# this file: monoco/core/agent/state.py
|
|
105
|
-
# root: monoco/../../
|
|
106
|
-
|
|
107
|
-
current_file = Path(__file__).resolve()
|
|
108
|
-
|
|
109
|
-
# Strategy 1: Development logic (Toolkit/scripts/check_agents.sh)
|
|
110
|
-
dev_path = current_file.parents[3] / "scripts" / "check_agents.sh"
|
|
111
|
-
if dev_path.exists():
|
|
112
|
-
return dev_path
|
|
113
|
-
|
|
114
|
-
# Strategy 2: If installed in site-packages, maybe we package scripts nearby?
|
|
115
|
-
# TODO: Define packaging strategy for scripts
|
|
116
|
-
|
|
105
|
+
"""[Deprecated] No longer used."""
|
|
117
106
|
return None
|
monoco/core/config.py
CHANGED
|
@@ -10,10 +10,7 @@ import asyncio
|
|
|
10
10
|
from watchdog.observers import Observer
|
|
11
11
|
from watchdog.events import FileSystemEventHandler
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
from typing import TYPE_CHECKING
|
|
15
|
-
if TYPE_CHECKING:
|
|
16
|
-
from monoco.core.integrations import AgentIntegration
|
|
13
|
+
from monoco.core.integrations import AgentIntegration
|
|
17
14
|
|
|
18
15
|
logger = logging.getLogger("monoco.core.config")
|
|
19
16
|
|
|
@@ -55,7 +52,7 @@ class AgentConfig(BaseModel):
|
|
|
55
52
|
targets: Optional[list[str]] = Field(default=None, description="Specific target files to inject into (e.g. .cursorrules)")
|
|
56
53
|
framework: Optional[str] = Field(default=None, description="Manually specified agent framework (cursor, windsurf, etc.)")
|
|
57
54
|
includes: Optional[list[str]] = Field(default=None, description="List of specific features to include in injection")
|
|
58
|
-
integrations: Optional[Dict[str,
|
|
55
|
+
integrations: Optional[Dict[str, "AgentIntegration"]] = Field(
|
|
59
56
|
default=None,
|
|
60
57
|
description="Custom agent framework integrations (overrides defaults from monoco.core.integrations)"
|
|
61
58
|
)
|
|
@@ -84,9 +81,13 @@ class MonocoConfig(BaseModel):
|
|
|
84
81
|
return base
|
|
85
82
|
|
|
86
83
|
@classmethod
|
|
87
|
-
def load(cls, project_root: Optional[str] = None) -> "MonocoConfig":
|
|
84
|
+
def load(cls, project_root: Optional[str] = None, require_project: bool = False) -> "MonocoConfig":
|
|
88
85
|
"""
|
|
89
86
|
Load configuration from multiple sources.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
project_root: Explicit root path. If None, uses CWD.
|
|
90
|
+
require_project: If True, raises error if .monoco directory is missing.
|
|
90
91
|
"""
|
|
91
92
|
# 1. Start with empty dict (will use defaults via Pydantic)
|
|
92
93
|
config_data = {}
|
|
@@ -96,12 +97,13 @@ class MonocoConfig(BaseModel):
|
|
|
96
97
|
|
|
97
98
|
# Determine project path
|
|
98
99
|
cwd = Path(project_root) if project_root else Path.cwd()
|
|
99
|
-
|
|
100
|
+
# FIX-0009: strict separation of workspace and project config
|
|
101
|
+
workspace_config_path = cwd / ".monoco" / "workspace.yaml"
|
|
102
|
+
project_config_path = cwd / ".monoco" / "project.yaml"
|
|
100
103
|
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
logger.warning(f"Legacy configuration found: {proj_path_legacy}. Please move it to .monoco/config.yaml")
|
|
104
|
+
# Strict Workspace Check
|
|
105
|
+
if require_project and not (cwd / ".monoco").exists():
|
|
106
|
+
raise FileNotFoundError(f"Monoco workspace not found in {cwd}. (No .monoco directory)")
|
|
105
107
|
|
|
106
108
|
# 3. Load User Config
|
|
107
109
|
if home_path.exists():
|
|
@@ -114,13 +116,35 @@ class MonocoConfig(BaseModel):
|
|
|
114
116
|
# We don't want to crash on config load fail, implementing simple warning equivalent
|
|
115
117
|
pass
|
|
116
118
|
|
|
117
|
-
# 4. Load Project Config
|
|
118
|
-
|
|
119
|
+
# 4. Load Project/Workspace Config
|
|
120
|
+
|
|
121
|
+
# 4a. Load workspace.yaml (Global Environment)
|
|
122
|
+
if workspace_config_path.exists():
|
|
123
|
+
try:
|
|
124
|
+
with open(workspace_config_path, "r") as f:
|
|
125
|
+
ws_config = yaml.safe_load(f)
|
|
126
|
+
if ws_config:
|
|
127
|
+
# workspace.yaml contains core, paths, i18n, ui, telemetry, agent
|
|
128
|
+
cls._deep_merge(config_data, ws_config)
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
# 4b. Load project.yaml (Identity)
|
|
133
|
+
if project_config_path.exists():
|
|
119
134
|
try:
|
|
120
|
-
with open(
|
|
121
|
-
|
|
122
|
-
if
|
|
123
|
-
|
|
135
|
+
with open(project_config_path, "r") as f:
|
|
136
|
+
pj_config = yaml.safe_load(f)
|
|
137
|
+
if pj_config:
|
|
138
|
+
# project.yaml contains 'project' fields directly? or under 'project' key?
|
|
139
|
+
# Design decision: project.yaml should be clean, e.g. "name: foo".
|
|
140
|
+
# But to simplify merging, let's check if it has a 'project' key or is flat.
|
|
141
|
+
if "project" in pj_config:
|
|
142
|
+
cls._deep_merge(config_data, pj_config)
|
|
143
|
+
else:
|
|
144
|
+
# Assume flat structure mapping to 'project' section
|
|
145
|
+
if "project" not in config_data:
|
|
146
|
+
config_data["project"] = {}
|
|
147
|
+
cls._deep_merge(config_data["project"], pj_config)
|
|
124
148
|
except Exception:
|
|
125
149
|
pass
|
|
126
150
|
|
|
@@ -130,10 +154,12 @@ class MonocoConfig(BaseModel):
|
|
|
130
154
|
# Global singleton
|
|
131
155
|
_settings = None
|
|
132
156
|
|
|
133
|
-
def get_config(project_root: Optional[str] = None) -> MonocoConfig:
|
|
157
|
+
def get_config(project_root: Optional[str] = None, require_project: bool = False) -> MonocoConfig:
|
|
134
158
|
global _settings
|
|
135
|
-
|
|
136
|
-
|
|
159
|
+
# If explicit root provided, always reload.
|
|
160
|
+
# If require_project is True, we must reload to ensure validation happens (in case a previous loose load occurred).
|
|
161
|
+
if _settings is None or project_root is not None or require_project:
|
|
162
|
+
_settings = MonocoConfig.load(project_root, require_project=require_project)
|
|
137
163
|
return _settings
|
|
138
164
|
|
|
139
165
|
class ConfigScope(str, Enum):
|
monoco/core/integrations.py
CHANGED
|
@@ -22,14 +22,59 @@ class AgentIntegration(BaseModel):
|
|
|
22
22
|
name: Human-readable name of the framework
|
|
23
23
|
system_prompt_file: Path to the system prompt/rules file relative to project root
|
|
24
24
|
skill_root_dir: Path to the skills directory relative to project root
|
|
25
|
+
bin_name: Optional name of the binary to check for (e.g., 'cursor', 'gemini')
|
|
26
|
+
version_cmd: Optional command to check version (e.g., '--version')
|
|
25
27
|
enabled: Whether this integration is active (default: True)
|
|
26
28
|
"""
|
|
27
29
|
key: str = Field(..., description="Unique framework identifier")
|
|
28
30
|
name: str = Field(..., description="Human-readable framework name")
|
|
29
31
|
system_prompt_file: str = Field(..., description="Path to system prompt file (relative to project root)")
|
|
30
32
|
skill_root_dir: str = Field(..., description="Path to skills directory (relative to project root)")
|
|
33
|
+
bin_name: Optional[str] = Field(None, description="Binary name to check for availability")
|
|
34
|
+
version_cmd: Optional[str] = Field(None, description="Command to check version")
|
|
31
35
|
enabled: bool = Field(default=True, description="Whether this integration is active")
|
|
32
36
|
|
|
37
|
+
def check_health(self) -> "AgentProviderHealth":
|
|
38
|
+
"""
|
|
39
|
+
Check health of this integration.
|
|
40
|
+
"""
|
|
41
|
+
import shutil
|
|
42
|
+
import subprocess
|
|
43
|
+
import time
|
|
44
|
+
|
|
45
|
+
if not self.bin_name:
|
|
46
|
+
return AgentProviderHealth(available=True) # If no binary required, assume ok
|
|
47
|
+
|
|
48
|
+
bin_path = shutil.which(self.bin_name)
|
|
49
|
+
if not bin_path:
|
|
50
|
+
return AgentProviderHealth(available=False, error=f"Binary '{self.bin_name}' not found in PATH")
|
|
51
|
+
|
|
52
|
+
if not self.version_cmd:
|
|
53
|
+
return AgentProviderHealth(available=True, path=bin_path)
|
|
54
|
+
|
|
55
|
+
start_time = time.time()
|
|
56
|
+
try:
|
|
57
|
+
# We use a shortcut to check version cmd
|
|
58
|
+
# Some tools might need e.g. 'cursor --version'
|
|
59
|
+
subprocess.run(
|
|
60
|
+
[self.bin_name] + self.version_cmd.split(),
|
|
61
|
+
check=True,
|
|
62
|
+
capture_output=True,
|
|
63
|
+
timeout=5
|
|
64
|
+
)
|
|
65
|
+
latency = int((time.time() - start_time) * 1000)
|
|
66
|
+
return AgentProviderHealth(available=True, path=bin_path, latency_ms=latency)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
return AgentProviderHealth(available=False, path=bin_path, error=str(e))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class AgentProviderHealth(BaseModel):
|
|
72
|
+
"""Health check result for an agent provider."""
|
|
73
|
+
available: bool
|
|
74
|
+
path: Optional[str] = None
|
|
75
|
+
error: Optional[str] = None
|
|
76
|
+
latency_ms: Optional[int] = None
|
|
77
|
+
|
|
33
78
|
|
|
34
79
|
# Default Integration Registry
|
|
35
80
|
DEFAULT_INTEGRATIONS: Dict[str, AgentIntegration] = {
|
|
@@ -38,24 +83,32 @@ DEFAULT_INTEGRATIONS: Dict[str, AgentIntegration] = {
|
|
|
38
83
|
name="Cursor",
|
|
39
84
|
system_prompt_file=".cursorrules",
|
|
40
85
|
skill_root_dir=".cursor/skills/",
|
|
86
|
+
bin_name="cursor",
|
|
87
|
+
version_cmd="--version",
|
|
41
88
|
),
|
|
42
89
|
"claude": AgentIntegration(
|
|
43
90
|
key="claude",
|
|
44
91
|
name="Claude Code",
|
|
45
92
|
system_prompt_file="CLAUDE.md",
|
|
46
93
|
skill_root_dir=".claude/skills/",
|
|
94
|
+
bin_name="claude",
|
|
95
|
+
version_cmd="--version",
|
|
47
96
|
),
|
|
48
97
|
"gemini": AgentIntegration(
|
|
49
98
|
key="gemini",
|
|
50
99
|
name="Gemini CLI",
|
|
51
100
|
system_prompt_file="GEMINI.md",
|
|
52
101
|
skill_root_dir=".gemini/skills/",
|
|
102
|
+
bin_name="gemini",
|
|
103
|
+
version_cmd="--version",
|
|
53
104
|
),
|
|
54
105
|
"qwen": AgentIntegration(
|
|
55
106
|
key="qwen",
|
|
56
107
|
name="Qwen Code",
|
|
57
108
|
system_prompt_file="QWEN.md",
|
|
58
109
|
skill_root_dir=".qwen/skills/",
|
|
110
|
+
bin_name="qwen",
|
|
111
|
+
version_cmd="--version",
|
|
59
112
|
),
|
|
60
113
|
"agent": AgentIntegration(
|
|
61
114
|
key="agent",
|
monoco/core/lsp.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
from typing import List, Optional, Union
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
class Position(BaseModel):
|
|
6
|
+
"""
|
|
7
|
+
Position in a text document expressed as zero-based line and character offset.
|
|
8
|
+
"""
|
|
9
|
+
line: int
|
|
10
|
+
character: int
|
|
11
|
+
|
|
12
|
+
def __lt__(self, other):
|
|
13
|
+
if self.line != other.line:
|
|
14
|
+
return self.line < other.line
|
|
15
|
+
return self.character < other.character
|
|
16
|
+
|
|
17
|
+
class Range(BaseModel):
|
|
18
|
+
"""
|
|
19
|
+
A range in a text document expressed as (zero-based) start and end positions.
|
|
20
|
+
"""
|
|
21
|
+
start: Position
|
|
22
|
+
end: Position
|
|
23
|
+
|
|
24
|
+
def __repr__(self):
|
|
25
|
+
return f"{self.start.line}:{self.start.character}-{self.end.line}:{self.end.character}"
|
|
26
|
+
|
|
27
|
+
class DiagnosticSeverity(IntEnum):
|
|
28
|
+
Error = 1
|
|
29
|
+
Warning = 2
|
|
30
|
+
Information = 3
|
|
31
|
+
Hint = 4
|
|
32
|
+
|
|
33
|
+
class DiagnosticRelatedInformation(BaseModel):
|
|
34
|
+
"""
|
|
35
|
+
Represents a related message and source code location for a diagnostic.
|
|
36
|
+
"""
|
|
37
|
+
# location: Location # Defined elsewhere or simplified here
|
|
38
|
+
message: str
|
|
39
|
+
|
|
40
|
+
class Diagnostic(BaseModel):
|
|
41
|
+
"""
|
|
42
|
+
Represents a diagnostic, such as a compiler error or warning.
|
|
43
|
+
"""
|
|
44
|
+
range: Range
|
|
45
|
+
severity: Optional[DiagnosticSeverity] = None
|
|
46
|
+
code: Optional[Union[int, str]] = None
|
|
47
|
+
source: Optional[str] = "monoco"
|
|
48
|
+
message: str
|
|
49
|
+
related_information: Optional[List[DiagnosticRelatedInformation]] = None
|
|
50
|
+
data: Optional[dict] = None # To carry extra info (e.g. for code actions)
|
|
51
|
+
|
|
52
|
+
def to_user_string(self) -> str:
|
|
53
|
+
"""Helper to format for CLI output"""
|
|
54
|
+
severity_map = {
|
|
55
|
+
1: "[red]Error[/red]",
|
|
56
|
+
2: "[yellow]Warning[/yellow]",
|
|
57
|
+
3: "[blue]Info[/blue]",
|
|
58
|
+
4: "[dim]Hint[/dim]"
|
|
59
|
+
}
|
|
60
|
+
sev = severity_map.get(self.severity, "Error")
|
|
61
|
+
return f"{sev}: {self.message} (Line {self.range.start.line + 1})"
|