monoco-toolkit 0.1.4__py3-none-any.whl → 0.1.6__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/core/config.py +60 -8
- monoco/core/feature.py +58 -0
- monoco/core/injection.py +196 -0
- monoco/core/integrations.py +181 -0
- monoco/core/registry.py +36 -0
- monoco/core/resources/en/AGENTS.md +8 -0
- monoco/core/resources/en/SKILL.md +66 -0
- monoco/core/resources/zh/AGENTS.md +8 -0
- monoco/core/resources/zh/SKILL.md +66 -0
- monoco/core/setup.py +40 -24
- monoco/core/skills.py +444 -0
- monoco/core/sync.py +224 -0
- monoco/core/workspace.py +2 -6
- monoco/daemon/services.py +1 -1
- monoco/features/config/commands.py +104 -44
- monoco/features/i18n/adapter.py +29 -0
- monoco/features/i18n/core.py +1 -11
- monoco/features/i18n/resources/en/AGENTS.md +8 -0
- monoco/features/i18n/resources/en/SKILL.md +94 -0
- monoco/features/i18n/resources/zh/AGENTS.md +8 -0
- monoco/features/i18n/resources/zh/SKILL.md +94 -0
- monoco/features/issue/adapter.py +34 -0
- monoco/features/issue/commands.py +8 -8
- monoco/features/issue/core.py +5 -16
- monoco/features/issue/migration.py +134 -0
- monoco/features/issue/models.py +5 -3
- monoco/features/issue/resources/en/AGENTS.md +9 -0
- monoco/features/issue/resources/en/SKILL.md +51 -0
- monoco/features/issue/resources/zh/AGENTS.md +9 -0
- monoco/features/issue/resources/zh/SKILL.md +85 -0
- monoco/features/spike/adapter.py +30 -0
- monoco/features/spike/core.py +3 -20
- monoco/features/spike/resources/en/AGENTS.md +7 -0
- monoco/features/spike/resources/en/SKILL.md +74 -0
- monoco/features/spike/resources/zh/AGENTS.md +7 -0
- monoco/features/spike/resources/zh/SKILL.md +74 -0
- monoco/main.py +4 -0
- {monoco_toolkit-0.1.4.dist-info → monoco_toolkit-0.1.6.dist-info}/METADATA +1 -1
- monoco_toolkit-0.1.6.dist-info/RECORD +62 -0
- monoco_toolkit-0.1.4.dist-info/RECORD +0 -36
- {monoco_toolkit-0.1.4.dist-info → monoco_toolkit-0.1.6.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.1.4.dist-info → monoco_toolkit-0.1.6.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.1.4.dist-info → monoco_toolkit-0.1.6.dist-info}/licenses/LICENSE +0 -0
monoco/core/config.py
CHANGED
|
@@ -2,8 +2,18 @@ import os
|
|
|
2
2
|
import yaml
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Optional, Dict, Any
|
|
5
|
+
from enum import Enum
|
|
5
6
|
from pydantic import BaseModel, Field
|
|
6
7
|
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
# Import AgentIntegration for type hints
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from monoco.core.integrations import AgentIntegration
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("monoco.core.config")
|
|
16
|
+
|
|
7
17
|
class PathsConfig(BaseModel):
|
|
8
18
|
"""Configuration for directory paths."""
|
|
9
19
|
root: str = Field(default=".", description="Project root directory")
|
|
@@ -37,6 +47,16 @@ class TelemetryConfig(BaseModel):
|
|
|
37
47
|
"""Configuration for Telemetry."""
|
|
38
48
|
enabled: Optional[bool] = Field(default=None, description="Whether telemetry is enabled")
|
|
39
49
|
|
|
50
|
+
class AgentConfig(BaseModel):
|
|
51
|
+
"""Configuration for Agent Environment Integration."""
|
|
52
|
+
targets: Optional[list[str]] = Field(default=None, description="Specific target files to inject into (e.g. .cursorrules)")
|
|
53
|
+
framework: Optional[str] = Field(default=None, description="Manually specified agent framework (cursor, windsurf, etc.)")
|
|
54
|
+
includes: Optional[list[str]] = Field(default=None, description="List of specific features to include in injection")
|
|
55
|
+
integrations: Optional[Dict[str, Any]] = Field(
|
|
56
|
+
default=None,
|
|
57
|
+
description="Custom agent framework integrations (overrides defaults from monoco.core.integrations)"
|
|
58
|
+
)
|
|
59
|
+
|
|
40
60
|
class MonocoConfig(BaseModel):
|
|
41
61
|
"""
|
|
42
62
|
Main Configuration Schema.
|
|
@@ -48,6 +68,7 @@ class MonocoConfig(BaseModel):
|
|
|
48
68
|
i18n: I18nConfig = Field(default_factory=I18nConfig)
|
|
49
69
|
ui: UIConfig = Field(default_factory=UIConfig)
|
|
50
70
|
telemetry: TelemetryConfig = Field(default_factory=TelemetryConfig)
|
|
71
|
+
agent: AgentConfig = Field(default_factory=AgentConfig)
|
|
51
72
|
|
|
52
73
|
@staticmethod
|
|
53
74
|
def _deep_merge(base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -73,7 +94,11 @@ class MonocoConfig(BaseModel):
|
|
|
73
94
|
# Determine project path
|
|
74
95
|
cwd = Path(project_root) if project_root else Path.cwd()
|
|
75
96
|
proj_path_hidden = cwd / ".monoco" / "config.yaml"
|
|
76
|
-
|
|
97
|
+
|
|
98
|
+
# [Legacy] Check for monoco.yaml and warn
|
|
99
|
+
proj_path_legacy = cwd / "monoco.yaml"
|
|
100
|
+
if proj_path_legacy.exists():
|
|
101
|
+
logger.warning(f"Legacy configuration found: {proj_path_legacy}. Please move it to .monoco/config.yaml")
|
|
77
102
|
|
|
78
103
|
# 3. Load User Config
|
|
79
104
|
if home_path.exists():
|
|
@@ -86,14 +111,10 @@ class MonocoConfig(BaseModel):
|
|
|
86
111
|
# We don't want to crash on config load fail, implementing simple warning equivalent
|
|
87
112
|
pass
|
|
88
113
|
|
|
89
|
-
# 4. Load Project Config (
|
|
90
|
-
|
|
91
|
-
proj_path_root if proj_path_root.exists() else None
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
if target_proj_conf:
|
|
114
|
+
# 4. Load Project Config (Only .monoco/config.yaml)
|
|
115
|
+
if proj_path_hidden.exists():
|
|
95
116
|
try:
|
|
96
|
-
with open(
|
|
117
|
+
with open(proj_path_hidden, "r") as f:
|
|
97
118
|
proj_config = yaml.safe_load(f)
|
|
98
119
|
if proj_config:
|
|
99
120
|
cls._deep_merge(config_data, proj_config)
|
|
@@ -111,3 +132,34 @@ def get_config(project_root: Optional[str] = None) -> MonocoConfig:
|
|
|
111
132
|
if _settings is None or project_root is not None:
|
|
112
133
|
_settings = MonocoConfig.load(project_root)
|
|
113
134
|
return _settings
|
|
135
|
+
|
|
136
|
+
class ConfigScope(str, Enum):
|
|
137
|
+
GLOBAL = "global"
|
|
138
|
+
PROJECT = "project"
|
|
139
|
+
|
|
140
|
+
def get_config_path(scope: ConfigScope, project_root: Optional[str] = None) -> Path:
|
|
141
|
+
"""Get the path to the configuration file for a given scope."""
|
|
142
|
+
if scope == ConfigScope.GLOBAL:
|
|
143
|
+
return Path.home() / ".monoco" / "config.yaml"
|
|
144
|
+
else:
|
|
145
|
+
cwd = Path(project_root) if project_root else Path.cwd()
|
|
146
|
+
return cwd / ".monoco" / "config.yaml"
|
|
147
|
+
|
|
148
|
+
def load_raw_config(scope: ConfigScope, project_root: Optional[str] = None) -> Dict[str, Any]:
|
|
149
|
+
"""Load raw configuration dictionary from a specific scope."""
|
|
150
|
+
path = get_config_path(scope, project_root)
|
|
151
|
+
if not path.exists():
|
|
152
|
+
return {}
|
|
153
|
+
try:
|
|
154
|
+
with open(path, "r") as f:
|
|
155
|
+
return yaml.safe_load(f) or {}
|
|
156
|
+
except Exception as e:
|
|
157
|
+
logger.warning(f"Failed to load config from {path}: {e}")
|
|
158
|
+
return {}
|
|
159
|
+
|
|
160
|
+
def save_raw_config(scope: ConfigScope, data: Dict[str, Any], project_root: Optional[str] = None) -> None:
|
|
161
|
+
"""Save raw configuration dictionary to a specific scope."""
|
|
162
|
+
path = get_config_path(scope, project_root)
|
|
163
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
with open(path, "w") as f:
|
|
165
|
+
yaml.dump(data, f, default_flow_style=False)
|
monoco/core/feature.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class IntegrationData:
|
|
8
|
+
"""
|
|
9
|
+
Data collection returned by a feature for integration into the Agent environment.
|
|
10
|
+
"""
|
|
11
|
+
# System Prompts to be injected into agent configuration (e.g., .cursorrules)
|
|
12
|
+
# Key: Section Title (e.g., "Issue Management"), Value: Markdown Content
|
|
13
|
+
system_prompts: Dict[str, str] = field(default_factory=dict)
|
|
14
|
+
|
|
15
|
+
# Paths to skill directories or files to be copied/symlinked
|
|
16
|
+
# DEPRECATED: Skill distribution is cancelled. Only prompts are synced.
|
|
17
|
+
skills: List[Path] = field(default_factory=list)
|
|
18
|
+
|
|
19
|
+
class MonocoFeature(ABC):
|
|
20
|
+
"""
|
|
21
|
+
Abstract base class for all Monoco features.
|
|
22
|
+
Features must implement this protocol to participate in init and sync lifecycles.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def name(self) -> str:
|
|
28
|
+
"""Unique identifier for the feature (e.g., 'issue', 'spike')."""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def initialize(self, root: Path, config: Dict) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Lifecycle hook: Physical Structure Initialization.
|
|
35
|
+
Called during `monoco init`.
|
|
36
|
+
Responsible for creating necessary directories, files, and config templates.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
root: The root directory of the project.
|
|
40
|
+
config: The full project configuration dictionary.
|
|
41
|
+
"""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def integrate(self, root: Path, config: Dict) -> IntegrationData:
|
|
46
|
+
"""
|
|
47
|
+
Lifecycle hook: Agent Environment Integration.
|
|
48
|
+
Called during `monoco sync`.
|
|
49
|
+
Responsible for returning data (prompts, skills) needed for the Agent Setup.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
root: The root directory of the project.
|
|
53
|
+
config: The full project configuration dictionary.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
IntegrationData object containing prompts and skills.
|
|
57
|
+
"""
|
|
58
|
+
pass
|
monoco/core/injection.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
class PromptInjector:
|
|
6
|
+
"""
|
|
7
|
+
Engine for injecting managed content into Markdown-like files (e.g., .cursorrules, GEMINI.md).
|
|
8
|
+
Maintains a 'Managed Block' defined by a specific header.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
MANAGED_HEADER = "## Monoco Toolkit"
|
|
12
|
+
|
|
13
|
+
def __init__(self, target_file: Path):
|
|
14
|
+
self.target_file = target_file
|
|
15
|
+
|
|
16
|
+
def inject(self, prompts: Dict[str, str]) -> bool:
|
|
17
|
+
"""
|
|
18
|
+
Injects the provided prompts into the target file.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
prompts: A dictionary where key is the section title and value is the content.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
True if changes were written, False otherwise.
|
|
25
|
+
"""
|
|
26
|
+
current_content = ""
|
|
27
|
+
if self.target_file.exists():
|
|
28
|
+
current_content = self.target_file.read_text(encoding="utf-8")
|
|
29
|
+
|
|
30
|
+
new_content = self._merge_content(current_content, prompts)
|
|
31
|
+
|
|
32
|
+
if new_content != current_content:
|
|
33
|
+
self.target_file.write_text(new_content, encoding="utf-8")
|
|
34
|
+
return True
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
def _merge_content(self, original: str, prompts: Dict[str, str]) -> str:
|
|
38
|
+
"""
|
|
39
|
+
Merges the generated prompts into the original content within the managed block.
|
|
40
|
+
"""
|
|
41
|
+
# 1. Generate the new managed block content
|
|
42
|
+
managed_block = [self.MANAGED_HEADER, ""]
|
|
43
|
+
managed_block.append("> **Auto-Generated**: This section is managed by Monoco. Do not edit manually.\n")
|
|
44
|
+
|
|
45
|
+
for title, content in prompts.items():
|
|
46
|
+
managed_block.append(f"### {title}")
|
|
47
|
+
managed_block.append("") # Blank line after header
|
|
48
|
+
|
|
49
|
+
# Sanitize content: remove leading header if it matches the title
|
|
50
|
+
clean_content = content.strip()
|
|
51
|
+
# Regex to match optional leading hash header matching the title (case insensitive)
|
|
52
|
+
# e.g. "### Issue Management" or "# Issue Management"
|
|
53
|
+
pattern = r"^(#+\s*)" + re.escape(title) + r"\s*\n"
|
|
54
|
+
match = re.match(pattern, clean_content, re.IGNORECASE)
|
|
55
|
+
|
|
56
|
+
if match:
|
|
57
|
+
clean_content = clean_content[match.end():].strip()
|
|
58
|
+
|
|
59
|
+
managed_block.append(clean_content)
|
|
60
|
+
managed_block.append("") # Blank line after section
|
|
61
|
+
|
|
62
|
+
managed_block_str = "\n".join(managed_block).strip() + "\n"
|
|
63
|
+
|
|
64
|
+
# 2. Find and replace/append in the original content
|
|
65
|
+
lines = original.splitlines()
|
|
66
|
+
start_idx = -1
|
|
67
|
+
end_idx = -1
|
|
68
|
+
|
|
69
|
+
# Find start
|
|
70
|
+
for i, line in enumerate(lines):
|
|
71
|
+
if line.strip() == self.MANAGED_HEADER:
|
|
72
|
+
start_idx = i
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
if start_idx == -1:
|
|
76
|
+
# Block not found, append to end
|
|
77
|
+
if original and not original.endswith("\n"):
|
|
78
|
+
return original + "\n\n" + managed_block_str
|
|
79
|
+
elif original:
|
|
80
|
+
return original + "\n" + managed_block_str
|
|
81
|
+
else:
|
|
82
|
+
return managed_block_str
|
|
83
|
+
|
|
84
|
+
# Find end: Look for next header of level 1 (assuming Managed Header is H1)
|
|
85
|
+
# Or EOF
|
|
86
|
+
# Note: If MANAGED_HEADER is "# ...", we look for next "# ..."
|
|
87
|
+
# But allow "## ..." as children.
|
|
88
|
+
|
|
89
|
+
header_level_match = re.match(r"^(#+)\s", self.MANAGED_HEADER)
|
|
90
|
+
header_level_prefix = header_level_match.group(1) if header_level_match else "#"
|
|
91
|
+
|
|
92
|
+
for i in range(start_idx + 1, len(lines)):
|
|
93
|
+
line = lines[i]
|
|
94
|
+
# Check if this line is a header of the same level or higher (fewer #s)
|
|
95
|
+
# e.g. if Managed is "###", then "#" and "##" are higher/parents, "###" is sibling.
|
|
96
|
+
# We treat siblings as end of block too.
|
|
97
|
+
if line.startswith("#"):
|
|
98
|
+
# Match regex to get level
|
|
99
|
+
match = re.match(r"^(#+)\s", line)
|
|
100
|
+
if match:
|
|
101
|
+
level = match.group(1)
|
|
102
|
+
if len(level) <= len(header_level_prefix):
|
|
103
|
+
end_idx = i
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
if end_idx == -1:
|
|
107
|
+
end_idx = len(lines)
|
|
108
|
+
|
|
109
|
+
# 3. Construct result
|
|
110
|
+
pre_block = "\n".join(lines[:start_idx])
|
|
111
|
+
post_block = "\n".join(lines[end_idx:])
|
|
112
|
+
|
|
113
|
+
result = pre_block
|
|
114
|
+
if result:
|
|
115
|
+
result += "\n\n"
|
|
116
|
+
|
|
117
|
+
result += managed_block_str
|
|
118
|
+
|
|
119
|
+
if post_block:
|
|
120
|
+
# Ensure separation if post block exists and isn't just empty lines
|
|
121
|
+
if post_block.strip():
|
|
122
|
+
result += "\n" + post_block
|
|
123
|
+
else:
|
|
124
|
+
result += post_block # Keep trailing newlines if any, or normalize?
|
|
125
|
+
|
|
126
|
+
return result.strip() + "\n"
|
|
127
|
+
|
|
128
|
+
def remove(self) -> bool:
|
|
129
|
+
"""
|
|
130
|
+
Removes the managed block from the target file.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
True if changes were written (block removed), False otherwise.
|
|
134
|
+
"""
|
|
135
|
+
if not self.target_file.exists():
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
current_content = self.target_file.read_text(encoding="utf-8")
|
|
139
|
+
lines = current_content.splitlines()
|
|
140
|
+
|
|
141
|
+
start_idx = -1
|
|
142
|
+
end_idx = -1
|
|
143
|
+
|
|
144
|
+
# Find start
|
|
145
|
+
for i, line in enumerate(lines):
|
|
146
|
+
if line.strip() == self.MANAGED_HEADER:
|
|
147
|
+
start_idx = i
|
|
148
|
+
break
|
|
149
|
+
|
|
150
|
+
if start_idx == -1:
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
# Find end: exact logic as in _merge_content
|
|
154
|
+
header_level_match = re.match(r"^(#+)\s", self.MANAGED_HEADER)
|
|
155
|
+
header_level_prefix = header_level_match.group(1) if header_level_match else "#"
|
|
156
|
+
|
|
157
|
+
for i in range(start_idx + 1, len(lines)):
|
|
158
|
+
line = lines[i]
|
|
159
|
+
if line.startswith("#"):
|
|
160
|
+
match = re.match(r"^(#+)\s", line)
|
|
161
|
+
if match:
|
|
162
|
+
level = match.group(1)
|
|
163
|
+
if len(level) <= len(header_level_prefix):
|
|
164
|
+
end_idx = i
|
|
165
|
+
break
|
|
166
|
+
|
|
167
|
+
if end_idx == -1:
|
|
168
|
+
end_idx = len(lines)
|
|
169
|
+
|
|
170
|
+
# Reconstruct content without the block
|
|
171
|
+
# We also need to be careful about surrounding newlines to avoid leaving gaps
|
|
172
|
+
|
|
173
|
+
# Check lines before start_idx
|
|
174
|
+
while start_idx > 0 and not lines[start_idx-1].strip():
|
|
175
|
+
start_idx -= 1
|
|
176
|
+
|
|
177
|
+
# Check lines after end_idx (optional, but good for cleanup)
|
|
178
|
+
# Usually end_idx points to the next header or EOF.
|
|
179
|
+
# If it points to next header, we keep it.
|
|
180
|
+
|
|
181
|
+
pre_block = lines[:start_idx]
|
|
182
|
+
post_block = lines[end_idx:]
|
|
183
|
+
|
|
184
|
+
# If we removed everything, the file might become empty or just newlines
|
|
185
|
+
|
|
186
|
+
new_lines = pre_block + post_block
|
|
187
|
+
if not new_lines:
|
|
188
|
+
new_content = ""
|
|
189
|
+
else:
|
|
190
|
+
new_content = "\n".join(new_lines).strip() + "\n"
|
|
191
|
+
|
|
192
|
+
if new_content != current_content:
|
|
193
|
+
self.target_file.write_text(new_content, encoding="utf-8")
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
return False
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core Integration Registry for Agent Frameworks.
|
|
3
|
+
|
|
4
|
+
This module provides a centralized registry for managing integrations with various
|
|
5
|
+
agent frameworks (Cursor, Claude, Gemini, Qwen, Antigravity, etc.).
|
|
6
|
+
|
|
7
|
+
It defines the standard structure for framework integrations and provides utilities
|
|
8
|
+
for detection and configuration.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AgentIntegration(BaseModel):
|
|
17
|
+
"""
|
|
18
|
+
Configuration for a single agent framework integration.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
key: Unique identifier for the framework (e.g., 'cursor', 'gemini')
|
|
22
|
+
name: Human-readable name of the framework
|
|
23
|
+
system_prompt_file: Path to the system prompt/rules file relative to project root
|
|
24
|
+
skill_root_dir: Path to the skills directory relative to project root
|
|
25
|
+
enabled: Whether this integration is active (default: True)
|
|
26
|
+
"""
|
|
27
|
+
key: str = Field(..., description="Unique framework identifier")
|
|
28
|
+
name: str = Field(..., description="Human-readable framework name")
|
|
29
|
+
system_prompt_file: str = Field(..., description="Path to system prompt file (relative to project root)")
|
|
30
|
+
skill_root_dir: str = Field(..., description="Path to skills directory (relative to project root)")
|
|
31
|
+
enabled: bool = Field(default=True, description="Whether this integration is active")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Default Integration Registry
|
|
35
|
+
DEFAULT_INTEGRATIONS: Dict[str, AgentIntegration] = {
|
|
36
|
+
"cursor": AgentIntegration(
|
|
37
|
+
key="cursor",
|
|
38
|
+
name="Cursor",
|
|
39
|
+
system_prompt_file=".cursorrules",
|
|
40
|
+
skill_root_dir=".cursor/skills/",
|
|
41
|
+
),
|
|
42
|
+
"claude": AgentIntegration(
|
|
43
|
+
key="claude",
|
|
44
|
+
name="Claude Code",
|
|
45
|
+
system_prompt_file="CLAUDE.md",
|
|
46
|
+
skill_root_dir=".claude/skills/",
|
|
47
|
+
),
|
|
48
|
+
"gemini": AgentIntegration(
|
|
49
|
+
key="gemini",
|
|
50
|
+
name="Gemini CLI",
|
|
51
|
+
system_prompt_file="GEMINI.md",
|
|
52
|
+
skill_root_dir=".gemini/skills/",
|
|
53
|
+
),
|
|
54
|
+
"qwen": AgentIntegration(
|
|
55
|
+
key="qwen",
|
|
56
|
+
name="Qwen Code",
|
|
57
|
+
system_prompt_file="QWEN.md",
|
|
58
|
+
skill_root_dir=".qwen/skills/",
|
|
59
|
+
),
|
|
60
|
+
"agent": AgentIntegration(
|
|
61
|
+
key="agent",
|
|
62
|
+
name="Antigravity",
|
|
63
|
+
system_prompt_file="GEMINI.md",
|
|
64
|
+
skill_root_dir=".agent/skills/",
|
|
65
|
+
),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_integration(
|
|
70
|
+
name: str,
|
|
71
|
+
config_overrides: Optional[Dict[str, AgentIntegration]] = None
|
|
72
|
+
) -> Optional[AgentIntegration]:
|
|
73
|
+
"""
|
|
74
|
+
Get an agent integration by name.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
name: The framework key (e.g., 'cursor', 'gemini')
|
|
78
|
+
config_overrides: Optional user-defined integrations from config
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
AgentIntegration if found, None otherwise
|
|
82
|
+
|
|
83
|
+
Priority:
|
|
84
|
+
1. User config overrides
|
|
85
|
+
2. Default registry
|
|
86
|
+
"""
|
|
87
|
+
# Check user overrides first
|
|
88
|
+
if config_overrides and name in config_overrides:
|
|
89
|
+
return config_overrides[name]
|
|
90
|
+
|
|
91
|
+
# Fall back to defaults
|
|
92
|
+
return DEFAULT_INTEGRATIONS.get(name)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_all_integrations(
|
|
96
|
+
config_overrides: Optional[Dict[str, AgentIntegration]] = None,
|
|
97
|
+
enabled_only: bool = True
|
|
98
|
+
) -> Dict[str, AgentIntegration]:
|
|
99
|
+
"""
|
|
100
|
+
Get all available integrations.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
config_overrides: Optional user-defined integrations from config
|
|
104
|
+
enabled_only: If True, only return enabled integrations
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Dictionary of all integrations (merged defaults + overrides)
|
|
108
|
+
"""
|
|
109
|
+
# Start with defaults
|
|
110
|
+
all_integrations = DEFAULT_INTEGRATIONS.copy()
|
|
111
|
+
|
|
112
|
+
# Merge user overrides
|
|
113
|
+
if config_overrides:
|
|
114
|
+
all_integrations.update(config_overrides)
|
|
115
|
+
|
|
116
|
+
# Filter by enabled status if requested
|
|
117
|
+
if enabled_only:
|
|
118
|
+
return {k: v for k, v in all_integrations.items() if v.enabled}
|
|
119
|
+
|
|
120
|
+
return all_integrations
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def detect_frameworks(root: Path) -> List[str]:
|
|
124
|
+
"""
|
|
125
|
+
Auto-detect which agent frameworks are present in the project.
|
|
126
|
+
|
|
127
|
+
Detection is based on the existence of characteristic files/directories
|
|
128
|
+
for each framework.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
root: Project root directory
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List of detected framework keys (e.g., ['cursor', 'gemini'])
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
>>> root = Path("/path/to/project")
|
|
138
|
+
>>> frameworks = detect_frameworks(root)
|
|
139
|
+
>>> print(frameworks)
|
|
140
|
+
['cursor', 'gemini']
|
|
141
|
+
"""
|
|
142
|
+
detected = []
|
|
143
|
+
|
|
144
|
+
for key, integration in DEFAULT_INTEGRATIONS.items():
|
|
145
|
+
# Check if system prompt file exists
|
|
146
|
+
prompt_file = root / integration.system_prompt_file
|
|
147
|
+
|
|
148
|
+
# Check if skill directory exists
|
|
149
|
+
skill_dir = root / integration.skill_root_dir
|
|
150
|
+
|
|
151
|
+
# Consider framework present if either exists
|
|
152
|
+
if prompt_file.exists() or skill_dir.exists():
|
|
153
|
+
detected.append(key)
|
|
154
|
+
|
|
155
|
+
return detected
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_active_integrations(
|
|
159
|
+
root: Path,
|
|
160
|
+
config_overrides: Optional[Dict[str, AgentIntegration]] = None,
|
|
161
|
+
auto_detect: bool = True
|
|
162
|
+
) -> Dict[str, AgentIntegration]:
|
|
163
|
+
"""
|
|
164
|
+
Get integrations that are both enabled and detected in the project.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
root: Project root directory
|
|
168
|
+
config_overrides: Optional user-defined integrations from config
|
|
169
|
+
auto_detect: If True, only return integrations detected in the project
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Dictionary of active integrations
|
|
173
|
+
"""
|
|
174
|
+
all_integrations = get_all_integrations(config_overrides, enabled_only=True)
|
|
175
|
+
|
|
176
|
+
if not auto_detect:
|
|
177
|
+
return all_integrations
|
|
178
|
+
|
|
179
|
+
# Filter by detection
|
|
180
|
+
detected_keys = detect_frameworks(root)
|
|
181
|
+
return {k: v for k, v in all_integrations.items() if k in detected_keys}
|
monoco/core/registry.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import Dict, List, Type
|
|
2
|
+
from monoco.core.feature import MonocoFeature
|
|
3
|
+
|
|
4
|
+
class FeatureRegistry:
|
|
5
|
+
_features: Dict[str, MonocoFeature] = {}
|
|
6
|
+
|
|
7
|
+
@classmethod
|
|
8
|
+
def register(cls, feature: MonocoFeature):
|
|
9
|
+
"""Register a feature instance."""
|
|
10
|
+
cls._features[feature.name] = feature
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def get_features(cls) -> List[MonocoFeature]:
|
|
14
|
+
"""Get all registered features."""
|
|
15
|
+
return list(cls._features.values())
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def get_feature(cls, name: str) -> MonocoFeature:
|
|
19
|
+
"""Get a specific feature by name."""
|
|
20
|
+
return cls._features.get(name)
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def load_defaults(cls):
|
|
24
|
+
"""
|
|
25
|
+
Load default core features.
|
|
26
|
+
TODO: In the future, this could be dynamic via entry points.
|
|
27
|
+
"""
|
|
28
|
+
# Import here to avoid circular dependencies at module level
|
|
29
|
+
from monoco.features.issue.adapter import IssueFeature
|
|
30
|
+
from monoco.features.spike.adapter import SpikeFeature
|
|
31
|
+
from monoco.features.i18n.adapter import I18nFeature
|
|
32
|
+
|
|
33
|
+
cls.register(IssueFeature())
|
|
34
|
+
cls.register(SpikeFeature())
|
|
35
|
+
cls.register(I18nFeature())
|
|
36
|
+
pass
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
### Monoco Core
|
|
2
|
+
|
|
3
|
+
Core toolkit commands for project management.
|
|
4
|
+
|
|
5
|
+
- **Init**: `monoco init` (Initialize new Monoco project)
|
|
6
|
+
- **Config**: `monoco config get|set <key> [value]` (Manage configuration)
|
|
7
|
+
- **Sync**: `monoco sync` (Synchronize with agent environments)
|
|
8
|
+
- **Uninstall**: `monoco uninstall` (Clean up agent integrations)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: monoco-core
|
|
3
|
+
description: Core skill for Monoco Toolkit. Provides essential commands for project initialization, configuration, and workspace management.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Monoco Core
|
|
7
|
+
|
|
8
|
+
Core functionality and commands for the Monoco Toolkit.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Monoco is a developer productivity toolkit that provides:
|
|
13
|
+
|
|
14
|
+
- **Project initialization** with standardized structure
|
|
15
|
+
- **Configuration management** at global and project levels
|
|
16
|
+
- **Workspace management** for multi-project setups
|
|
17
|
+
|
|
18
|
+
## Key Commands
|
|
19
|
+
|
|
20
|
+
### Project Setup
|
|
21
|
+
|
|
22
|
+
- **`monoco init`**: Initialize a new Monoco project
|
|
23
|
+
- Creates `.monoco/` directory with default configuration
|
|
24
|
+
- Sets up project structure (Issues/, .references/, etc.)
|
|
25
|
+
- Generates initial documentation
|
|
26
|
+
|
|
27
|
+
### Configuration
|
|
28
|
+
|
|
29
|
+
- **`monoco config`**: Manage configuration
|
|
30
|
+
- `monoco config get <key>`: View configuration value
|
|
31
|
+
- `monoco config set <key> <value>`: Update configuration
|
|
32
|
+
- Supports both global (`~/.monoco/config.yaml`) and project (`.monoco/config.yaml`) scopes
|
|
33
|
+
|
|
34
|
+
### Agent Integration
|
|
35
|
+
|
|
36
|
+
- **`monoco sync`**: Synchronize with agent environments
|
|
37
|
+
|
|
38
|
+
- Injects system prompts into agent configuration files (GEMINI.md, CLAUDE.md, etc.)
|
|
39
|
+
- Distributes skills to agent framework directories
|
|
40
|
+
- Respects language configuration from `i18n.source_lang`
|
|
41
|
+
|
|
42
|
+
- **`monoco uninstall`**: Clean up agent integrations
|
|
43
|
+
- Removes managed blocks from agent configuration files
|
|
44
|
+
- Cleans up distributed skills
|
|
45
|
+
|
|
46
|
+
## Configuration Structure
|
|
47
|
+
|
|
48
|
+
Configuration is stored in YAML format at:
|
|
49
|
+
|
|
50
|
+
- **Global**: `~/.monoco/config.yaml`
|
|
51
|
+
- **Project**: `.monoco/config.yaml`
|
|
52
|
+
|
|
53
|
+
Key configuration sections:
|
|
54
|
+
|
|
55
|
+
- `core`: Editor, log level, author
|
|
56
|
+
- `paths`: Directory paths (issues, spikes, specs)
|
|
57
|
+
- `project`: Project metadata, spike repos, workspace members
|
|
58
|
+
- `i18n`: Internationalization settings
|
|
59
|
+
- `agent`: Agent framework integration settings
|
|
60
|
+
|
|
61
|
+
## Best Practices
|
|
62
|
+
|
|
63
|
+
1. **Use CLI commands** instead of manual file editing when possible
|
|
64
|
+
2. **Run `monoco sync`** after configuration changes to update agent environments
|
|
65
|
+
3. **Commit `.monoco/config.yaml`** to version control for team consistency
|
|
66
|
+
4. **Keep global config minimal** - most settings should be project-specific
|