monoco-toolkit 0.2.8__py3-none-any.whl → 0.3.0__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/project.py +35 -31
- monoco/cli/workspace.py +26 -16
- monoco/core/agent/__init__.py +0 -2
- monoco/core/agent/action.py +44 -20
- monoco/core/agent/adapters.py +20 -16
- monoco/core/agent/protocol.py +5 -4
- monoco/core/agent/state.py +21 -21
- monoco/core/config.py +90 -33
- monoco/core/execution.py +21 -16
- monoco/core/feature.py +8 -5
- monoco/core/git.py +61 -30
- monoco/core/hooks.py +57 -0
- monoco/core/injection.py +47 -44
- monoco/core/integrations.py +50 -35
- monoco/core/lsp.py +12 -1
- monoco/core/output.py +35 -16
- monoco/core/registry.py +3 -2
- monoco/core/setup.py +190 -124
- monoco/core/skills.py +121 -107
- monoco/core/state.py +12 -10
- monoco/core/sync.py +85 -56
- monoco/core/telemetry.py +10 -6
- monoco/core/workspace.py +26 -19
- monoco/daemon/app.py +123 -79
- monoco/daemon/commands.py +14 -13
- monoco/daemon/models.py +11 -3
- monoco/daemon/reproduce_stats.py +8 -8
- monoco/daemon/services.py +32 -33
- monoco/daemon/stats.py +59 -40
- monoco/features/config/commands.py +38 -25
- monoco/features/i18n/adapter.py +4 -5
- monoco/features/i18n/commands.py +83 -49
- monoco/features/i18n/core.py +94 -54
- monoco/features/issue/adapter.py +6 -7
- monoco/features/issue/commands.py +468 -272
- monoco/features/issue/core.py +419 -312
- monoco/features/issue/domain/lifecycle.py +33 -23
- monoco/features/issue/domain/models.py +71 -38
- monoco/features/issue/domain/parser.py +92 -69
- monoco/features/issue/domain/workspace.py +19 -16
- monoco/features/issue/engine/__init__.py +3 -3
- monoco/features/issue/engine/config.py +18 -25
- monoco/features/issue/engine/machine.py +72 -39
- monoco/features/issue/engine/models.py +4 -2
- monoco/features/issue/linter.py +287 -157
- monoco/features/issue/lsp/definition.py +26 -19
- monoco/features/issue/migration.py +45 -34
- monoco/features/issue/models.py +29 -13
- monoco/features/issue/monitor.py +24 -8
- monoco/features/issue/resources/en/SKILL.md +6 -2
- monoco/features/issue/validator.py +383 -208
- monoco/features/skills/__init__.py +0 -1
- monoco/features/skills/core.py +24 -18
- monoco/features/spike/adapter.py +4 -5
- monoco/features/spike/commands.py +51 -38
- monoco/features/spike/core.py +24 -16
- monoco/main.py +34 -21
- {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/METADATA +1 -1
- monoco_toolkit-0.3.0.dist-info/RECORD +84 -0
- monoco_toolkit-0.2.8.dist-info/RECORD +0 -83
- {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/licenses/LICENSE +0 -0
monoco/core/integrations.py
CHANGED
|
@@ -16,7 +16,7 @@ from pydantic import BaseModel, Field
|
|
|
16
16
|
class AgentIntegration(BaseModel):
|
|
17
17
|
"""
|
|
18
18
|
Configuration for a single agent framework integration.
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
Attributes:
|
|
21
21
|
key: Unique identifier for the framework (e.g., 'cursor', 'gemini')
|
|
22
22
|
name: Human-readable name of the framework
|
|
@@ -26,13 +26,22 @@ class AgentIntegration(BaseModel):
|
|
|
26
26
|
version_cmd: Optional command to check version (e.g., '--version')
|
|
27
27
|
enabled: Whether this integration is active (default: True)
|
|
28
28
|
"""
|
|
29
|
+
|
|
29
30
|
key: str = Field(..., description="Unique framework identifier")
|
|
30
31
|
name: str = Field(..., description="Human-readable framework name")
|
|
31
|
-
system_prompt_file: str = Field(
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
system_prompt_file: str = Field(
|
|
33
|
+
..., description="Path to system prompt file (relative to project root)"
|
|
34
|
+
)
|
|
35
|
+
skill_root_dir: str = Field(
|
|
36
|
+
..., description="Path to skills directory (relative to project root)"
|
|
37
|
+
)
|
|
38
|
+
bin_name: Optional[str] = Field(
|
|
39
|
+
None, description="Binary name to check for availability"
|
|
40
|
+
)
|
|
34
41
|
version_cmd: Optional[str] = Field(None, description="Command to check version")
|
|
35
|
-
enabled: bool = Field(
|
|
42
|
+
enabled: bool = Field(
|
|
43
|
+
default=True, description="Whether this integration is active"
|
|
44
|
+
)
|
|
36
45
|
|
|
37
46
|
def check_health(self) -> "AgentProviderHealth":
|
|
38
47
|
"""
|
|
@@ -43,14 +52,18 @@ class AgentIntegration(BaseModel):
|
|
|
43
52
|
import time
|
|
44
53
|
|
|
45
54
|
if not self.bin_name:
|
|
46
|
-
return AgentProviderHealth(
|
|
55
|
+
return AgentProviderHealth(
|
|
56
|
+
available=True
|
|
57
|
+
) # If no binary required, assume ok
|
|
47
58
|
|
|
48
59
|
bin_path = shutil.which(self.bin_name)
|
|
49
60
|
if not bin_path:
|
|
50
|
-
return AgentProviderHealth(
|
|
61
|
+
return AgentProviderHealth(
|
|
62
|
+
available=False, error=f"Binary '{self.bin_name}' not found in PATH"
|
|
63
|
+
)
|
|
51
64
|
|
|
52
65
|
if not self.version_cmd:
|
|
53
|
-
|
|
66
|
+
return AgentProviderHealth(available=True, path=bin_path)
|
|
54
67
|
|
|
55
68
|
start_time = time.time()
|
|
56
69
|
try:
|
|
@@ -60,16 +73,19 @@ class AgentIntegration(BaseModel):
|
|
|
60
73
|
[self.bin_name] + self.version_cmd.split(),
|
|
61
74
|
check=True,
|
|
62
75
|
capture_output=True,
|
|
63
|
-
timeout=5
|
|
76
|
+
timeout=5,
|
|
64
77
|
)
|
|
65
78
|
latency = int((time.time() - start_time) * 1000)
|
|
66
|
-
return AgentProviderHealth(
|
|
79
|
+
return AgentProviderHealth(
|
|
80
|
+
available=True, path=bin_path, latency_ms=latency
|
|
81
|
+
)
|
|
67
82
|
except Exception as e:
|
|
68
83
|
return AgentProviderHealth(available=False, path=bin_path, error=str(e))
|
|
69
84
|
|
|
70
85
|
|
|
71
86
|
class AgentProviderHealth(BaseModel):
|
|
72
87
|
"""Health check result for an agent provider."""
|
|
88
|
+
|
|
73
89
|
available: bool
|
|
74
90
|
path: Optional[str] = None
|
|
75
91
|
error: Optional[str] = None
|
|
@@ -128,19 +144,18 @@ DEFAULT_INTEGRATIONS: Dict[str, AgentIntegration] = {
|
|
|
128
144
|
|
|
129
145
|
|
|
130
146
|
def get_integration(
|
|
131
|
-
name: str,
|
|
132
|
-
config_overrides: Optional[Dict[str, AgentIntegration]] = None
|
|
147
|
+
name: str, config_overrides: Optional[Dict[str, AgentIntegration]] = None
|
|
133
148
|
) -> Optional[AgentIntegration]:
|
|
134
149
|
"""
|
|
135
150
|
Get an agent integration by name.
|
|
136
|
-
|
|
151
|
+
|
|
137
152
|
Args:
|
|
138
153
|
name: The framework key (e.g., 'cursor', 'gemini')
|
|
139
154
|
config_overrides: Optional user-defined integrations from config
|
|
140
|
-
|
|
155
|
+
|
|
141
156
|
Returns:
|
|
142
157
|
AgentIntegration if found, None otherwise
|
|
143
|
-
|
|
158
|
+
|
|
144
159
|
Priority:
|
|
145
160
|
1. User config overrides
|
|
146
161
|
2. Default registry
|
|
@@ -148,52 +163,52 @@ def get_integration(
|
|
|
148
163
|
# Check user overrides first
|
|
149
164
|
if config_overrides and name in config_overrides:
|
|
150
165
|
return config_overrides[name]
|
|
151
|
-
|
|
166
|
+
|
|
152
167
|
# Fall back to defaults
|
|
153
168
|
return DEFAULT_INTEGRATIONS.get(name)
|
|
154
169
|
|
|
155
170
|
|
|
156
171
|
def get_all_integrations(
|
|
157
172
|
config_overrides: Optional[Dict[str, AgentIntegration]] = None,
|
|
158
|
-
enabled_only: bool = True
|
|
173
|
+
enabled_only: bool = True,
|
|
159
174
|
) -> Dict[str, AgentIntegration]:
|
|
160
175
|
"""
|
|
161
176
|
Get all available integrations.
|
|
162
|
-
|
|
177
|
+
|
|
163
178
|
Args:
|
|
164
179
|
config_overrides: Optional user-defined integrations from config
|
|
165
180
|
enabled_only: If True, only return enabled integrations
|
|
166
|
-
|
|
181
|
+
|
|
167
182
|
Returns:
|
|
168
183
|
Dictionary of all integrations (merged defaults + overrides)
|
|
169
184
|
"""
|
|
170
185
|
# Start with defaults
|
|
171
186
|
all_integrations = DEFAULT_INTEGRATIONS.copy()
|
|
172
|
-
|
|
187
|
+
|
|
173
188
|
# Merge user overrides
|
|
174
189
|
if config_overrides:
|
|
175
190
|
all_integrations.update(config_overrides)
|
|
176
|
-
|
|
191
|
+
|
|
177
192
|
# Filter by enabled status if requested
|
|
178
193
|
if enabled_only:
|
|
179
194
|
return {k: v for k, v in all_integrations.items() if v.enabled}
|
|
180
|
-
|
|
195
|
+
|
|
181
196
|
return all_integrations
|
|
182
197
|
|
|
183
198
|
|
|
184
199
|
def detect_frameworks(root: Path) -> List[str]:
|
|
185
200
|
"""
|
|
186
201
|
Auto-detect which agent frameworks are present in the project.
|
|
187
|
-
|
|
202
|
+
|
|
188
203
|
Detection is based on the existence of characteristic files/directories
|
|
189
204
|
for each framework.
|
|
190
|
-
|
|
205
|
+
|
|
191
206
|
Args:
|
|
192
207
|
root: Project root directory
|
|
193
|
-
|
|
208
|
+
|
|
194
209
|
Returns:
|
|
195
210
|
List of detected framework keys (e.g., ['cursor', 'gemini'])
|
|
196
|
-
|
|
211
|
+
|
|
197
212
|
Example:
|
|
198
213
|
>>> root = Path("/path/to/project")
|
|
199
214
|
>>> frameworks = detect_frameworks(root)
|
|
@@ -201,42 +216,42 @@ def detect_frameworks(root: Path) -> List[str]:
|
|
|
201
216
|
['cursor', 'gemini']
|
|
202
217
|
"""
|
|
203
218
|
detected = []
|
|
204
|
-
|
|
219
|
+
|
|
205
220
|
for key, integration in DEFAULT_INTEGRATIONS.items():
|
|
206
221
|
# Check if system prompt file exists
|
|
207
222
|
prompt_file = root / integration.system_prompt_file
|
|
208
|
-
|
|
223
|
+
|
|
209
224
|
# Check if skill directory exists
|
|
210
225
|
skill_dir = root / integration.skill_root_dir
|
|
211
|
-
|
|
226
|
+
|
|
212
227
|
# Consider framework present if either exists
|
|
213
228
|
if prompt_file.exists() or skill_dir.exists():
|
|
214
229
|
detected.append(key)
|
|
215
|
-
|
|
230
|
+
|
|
216
231
|
return detected
|
|
217
232
|
|
|
218
233
|
|
|
219
234
|
def get_active_integrations(
|
|
220
235
|
root: Path,
|
|
221
236
|
config_overrides: Optional[Dict[str, AgentIntegration]] = None,
|
|
222
|
-
auto_detect: bool = True
|
|
237
|
+
auto_detect: bool = True,
|
|
223
238
|
) -> Dict[str, AgentIntegration]:
|
|
224
239
|
"""
|
|
225
240
|
Get integrations that are both enabled and detected in the project.
|
|
226
|
-
|
|
241
|
+
|
|
227
242
|
Args:
|
|
228
243
|
root: Project root directory
|
|
229
244
|
config_overrides: Optional user-defined integrations from config
|
|
230
245
|
auto_detect: If True, only return integrations detected in the project
|
|
231
|
-
|
|
246
|
+
|
|
232
247
|
Returns:
|
|
233
248
|
Dictionary of active integrations
|
|
234
249
|
"""
|
|
235
250
|
all_integrations = get_all_integrations(config_overrides, enabled_only=True)
|
|
236
|
-
|
|
251
|
+
|
|
237
252
|
if not auto_detect:
|
|
238
253
|
return all_integrations
|
|
239
|
-
|
|
254
|
+
|
|
240
255
|
# Filter by detection
|
|
241
256
|
detected_keys = detect_frameworks(root)
|
|
242
257
|
return {k: v for k, v in all_integrations.items() if k in detected_keys}
|
monoco/core/lsp.py
CHANGED
|
@@ -2,10 +2,12 @@ from enum import IntEnum
|
|
|
2
2
|
from typing import List, Optional, Union
|
|
3
3
|
from pydantic import BaseModel
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
class Position(BaseModel):
|
|
6
7
|
"""
|
|
7
8
|
Position in a text document expressed as zero-based line and character offset.
|
|
8
9
|
"""
|
|
10
|
+
|
|
9
11
|
line: int
|
|
10
12
|
character: int
|
|
11
13
|
|
|
@@ -14,40 +16,49 @@ class Position(BaseModel):
|
|
|
14
16
|
return self.line < other.line
|
|
15
17
|
return self.character < other.character
|
|
16
18
|
|
|
19
|
+
|
|
17
20
|
class Range(BaseModel):
|
|
18
21
|
"""
|
|
19
22
|
A range in a text document expressed as (zero-based) start and end positions.
|
|
20
23
|
"""
|
|
24
|
+
|
|
21
25
|
start: Position
|
|
22
26
|
end: Position
|
|
23
27
|
|
|
24
28
|
def __repr__(self):
|
|
25
29
|
return f"{self.start.line}:{self.start.character}-{self.end.line}:{self.end.character}"
|
|
26
30
|
|
|
31
|
+
|
|
27
32
|
class Location(BaseModel):
|
|
28
33
|
"""
|
|
29
34
|
Represents a location inside a resource, such as a line of code inside a text file.
|
|
30
35
|
"""
|
|
36
|
+
|
|
31
37
|
uri: str
|
|
32
38
|
range: Range
|
|
33
39
|
|
|
40
|
+
|
|
34
41
|
class DiagnosticSeverity(IntEnum):
|
|
35
42
|
Error = 1
|
|
36
43
|
Warning = 2
|
|
37
44
|
Information = 3
|
|
38
45
|
Hint = 4
|
|
39
46
|
|
|
47
|
+
|
|
40
48
|
class DiagnosticRelatedInformation(BaseModel):
|
|
41
49
|
"""
|
|
42
50
|
Represents a related message and source code location for a diagnostic.
|
|
43
51
|
"""
|
|
52
|
+
|
|
44
53
|
# location: Location # Defined elsewhere or simplified here
|
|
45
54
|
message: str
|
|
46
55
|
|
|
56
|
+
|
|
47
57
|
class Diagnostic(BaseModel):
|
|
48
58
|
"""
|
|
49
59
|
Represents a diagnostic, such as a compiler error or warning.
|
|
50
60
|
"""
|
|
61
|
+
|
|
51
62
|
range: Range
|
|
52
63
|
severity: Optional[DiagnosticSeverity] = None
|
|
53
64
|
code: Optional[Union[int, str]] = None
|
|
@@ -62,7 +73,7 @@ class Diagnostic(BaseModel):
|
|
|
62
73
|
1: "[red]Error[/red]",
|
|
63
74
|
2: "[yellow]Warning[/yellow]",
|
|
64
75
|
3: "[blue]Info[/blue]",
|
|
65
|
-
4: "[dim]Hint[/dim]"
|
|
76
|
+
4: "[dim]Hint[/dim]",
|
|
66
77
|
}
|
|
67
78
|
sev = severity_map.get(self.severity, "Error")
|
|
68
79
|
return f"{sev}: {self.message} (Line {self.range.start.line + 1})"
|
monoco/core/output.py
CHANGED
|
@@ -7,12 +7,20 @@ from rich.console import Console
|
|
|
7
7
|
from rich.table import Table
|
|
8
8
|
from rich import print as rprint
|
|
9
9
|
|
|
10
|
+
|
|
10
11
|
def _set_agent_mode(value: bool):
|
|
11
12
|
if value:
|
|
12
13
|
os.environ["AGENT_FLAG"] = "true"
|
|
13
14
|
|
|
15
|
+
|
|
14
16
|
# Reusable dependency for commands
|
|
15
|
-
AgentOutput = Annotated[
|
|
17
|
+
AgentOutput = Annotated[
|
|
18
|
+
bool,
|
|
19
|
+
typer.Option(
|
|
20
|
+
"--json", help="Output in compact JSON for Agents", callback=_set_agent_mode
|
|
21
|
+
),
|
|
22
|
+
]
|
|
23
|
+
|
|
16
24
|
|
|
17
25
|
class OutputManager:
|
|
18
26
|
"""
|
|
@@ -27,11 +35,14 @@ class OutputManager:
|
|
|
27
35
|
1. Environment variable AGENT_FLAG=true (or 1)
|
|
28
36
|
2. Environment variable MONOCO_AGENT=true (or 1)
|
|
29
37
|
"""
|
|
30
|
-
return os.getenv("AGENT_FLAG", "").lower() in ("true", "1") or
|
|
31
|
-
|
|
38
|
+
return os.getenv("AGENT_FLAG", "").lower() in ("true", "1") or os.getenv(
|
|
39
|
+
"MONOCO_AGENT", ""
|
|
40
|
+
).lower() in ("true", "1")
|
|
32
41
|
|
|
33
42
|
@staticmethod
|
|
34
|
-
def print(
|
|
43
|
+
def print(
|
|
44
|
+
data: Union[BaseModel, List[BaseModel], dict, list, str], title: str = ""
|
|
45
|
+
):
|
|
35
46
|
"""
|
|
36
47
|
Dual frontend dispatcher.
|
|
37
48
|
"""
|
|
@@ -58,20 +69,27 @@ class OutputManager:
|
|
|
58
69
|
"""
|
|
59
70
|
if isinstance(data, BaseModel):
|
|
60
71
|
print(data.model_dump_json(exclude_none=True))
|
|
61
|
-
elif isinstance(data, list) and all(
|
|
72
|
+
elif isinstance(data, list) and all(
|
|
73
|
+
isinstance(item, BaseModel) for item in data
|
|
74
|
+
):
|
|
62
75
|
# Pydantic v2 adapter for list of models
|
|
63
|
-
print(
|
|
76
|
+
print(
|
|
77
|
+
json.dumps(
|
|
78
|
+
[item.model_dump(mode="json", exclude_none=True) for item in data],
|
|
79
|
+
separators=(",", ":"),
|
|
80
|
+
)
|
|
81
|
+
)
|
|
64
82
|
else:
|
|
65
83
|
# Fallback for dicts/lists/primitives
|
|
66
84
|
def _encoder(obj):
|
|
67
85
|
if isinstance(obj, BaseModel):
|
|
68
|
-
return obj.model_dump(mode=
|
|
69
|
-
if hasattr(obj,
|
|
70
|
-
|
|
86
|
+
return obj.model_dump(mode="json", exclude_none=True)
|
|
87
|
+
if hasattr(obj, "value"): # Enum support
|
|
88
|
+
return obj.value
|
|
71
89
|
return str(obj)
|
|
72
90
|
|
|
73
91
|
try:
|
|
74
|
-
print(json.dumps(data, separators=(
|
|
92
|
+
print(json.dumps(data, separators=(",", ":"), default=_encoder))
|
|
75
93
|
except TypeError:
|
|
76
94
|
print(str(data))
|
|
77
95
|
|
|
@@ -81,7 +99,7 @@ class OutputManager:
|
|
|
81
99
|
Human channel: Visual priority.
|
|
82
100
|
"""
|
|
83
101
|
console = Console()
|
|
84
|
-
|
|
102
|
+
|
|
85
103
|
if title:
|
|
86
104
|
console.rule(f"[bold blue]{title}[/bold blue]")
|
|
87
105
|
|
|
@@ -92,24 +110,25 @@ class OutputManager:
|
|
|
92
110
|
# Special handling for Lists of Pydantic Models -> Table
|
|
93
111
|
if isinstance(data, list) and data and isinstance(data[0], BaseModel):
|
|
94
112
|
table = Table(show_header=True, header_style="bold magenta")
|
|
95
|
-
|
|
113
|
+
|
|
96
114
|
# Introspect fields from the first item
|
|
97
115
|
model_type = type(data[0])
|
|
98
116
|
fields = model_type.model_fields.keys()
|
|
99
|
-
|
|
117
|
+
|
|
100
118
|
for field in fields:
|
|
101
119
|
table.add_column(field.replace("_", " ").title())
|
|
102
|
-
|
|
120
|
+
|
|
103
121
|
for item in data:
|
|
104
122
|
row = [str(getattr(item, field)) for field in fields]
|
|
105
123
|
table.add_row(*row)
|
|
106
|
-
|
|
124
|
+
|
|
107
125
|
console.print(table)
|
|
108
126
|
return
|
|
109
|
-
|
|
127
|
+
|
|
110
128
|
# Fallback to rich pretty print
|
|
111
129
|
rprint(data)
|
|
112
130
|
|
|
131
|
+
|
|
113
132
|
# Global helper
|
|
114
133
|
print_output = OutputManager.print
|
|
115
134
|
print_error = OutputManager.error
|
monoco/core/registry.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
from typing import Dict, List
|
|
1
|
+
from typing import Dict, List
|
|
2
2
|
from monoco.core.feature import MonocoFeature
|
|
3
3
|
|
|
4
|
+
|
|
4
5
|
class FeatureRegistry:
|
|
5
6
|
_features: Dict[str, MonocoFeature] = {}
|
|
6
7
|
|
|
@@ -29,7 +30,7 @@ class FeatureRegistry:
|
|
|
29
30
|
from monoco.features.issue.adapter import IssueFeature
|
|
30
31
|
from monoco.features.spike.adapter import SpikeFeature
|
|
31
32
|
from monoco.features.i18n.adapter import I18nFeature
|
|
32
|
-
|
|
33
|
+
|
|
33
34
|
cls.register(IssueFeature())
|
|
34
35
|
cls.register(SpikeFeature())
|
|
35
36
|
cls.register(I18nFeature())
|