monoco-toolkit 0.1.0__py3-none-any.whl → 0.2.5__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 +87 -0
- monoco/cli/workspace.py +46 -0
- monoco/core/agent/__init__.py +5 -0
- monoco/core/agent/action.py +144 -0
- monoco/core/agent/adapters.py +106 -0
- monoco/core/agent/protocol.py +31 -0
- monoco/core/agent/state.py +106 -0
- monoco/core/config.py +152 -17
- monoco/core/execution.py +62 -0
- monoco/core/feature.py +58 -0
- monoco/core/git.py +51 -2
- monoco/core/injection.py +196 -0
- monoco/core/integrations.py +234 -0
- monoco/core/lsp.py +61 -0
- monoco/core/output.py +13 -2
- 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 +88 -110
- monoco/core/skills.py +444 -0
- monoco/core/state.py +53 -0
- monoco/core/sync.py +224 -0
- monoco/core/telemetry.py +4 -1
- monoco/core/workspace.py +85 -20
- monoco/daemon/app.py +127 -58
- monoco/daemon/models.py +4 -0
- monoco/daemon/services.py +56 -155
- monoco/features/agent/commands.py +166 -0
- monoco/features/agent/doctor.py +30 -0
- monoco/features/config/commands.py +125 -44
- monoco/features/i18n/adapter.py +29 -0
- monoco/features/i18n/commands.py +89 -10
- monoco/features/i18n/core.py +113 -27
- 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 +183 -65
- monoco/features/issue/core.py +172 -77
- monoco/features/issue/linter.py +215 -116
- monoco/features/issue/migration.py +134 -0
- monoco/features/issue/models.py +23 -19
- monoco/features/issue/monitor.py +94 -0
- monoco/features/issue/resources/en/AGENTS.md +15 -0
- monoco/features/issue/resources/en/SKILL.md +87 -0
- monoco/features/issue/resources/zh/AGENTS.md +15 -0
- monoco/features/issue/resources/zh/SKILL.md +114 -0
- monoco/features/issue/validator.py +269 -0
- monoco/features/pty/core.py +185 -0
- monoco/features/pty/router.py +138 -0
- monoco/features/pty/server.py +56 -0
- monoco/features/spike/adapter.py +30 -0
- monoco/features/spike/commands.py +45 -24
- monoco/features/spike/core.py +4 -21
- 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 +115 -2
- {monoco_toolkit-0.1.0.dist-info → monoco_toolkit-0.2.5.dist-info}/METADATA +10 -3
- monoco_toolkit-0.2.5.dist-info/RECORD +77 -0
- monoco_toolkit-0.1.0.dist-info/RECORD +0 -33
- {monoco_toolkit-0.1.0.dist-info → monoco_toolkit-0.2.5.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.1.0.dist-info → monoco_toolkit-0.2.5.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.1.0.dist-info → monoco_toolkit-0.2.5.dist-info}/licenses/LICENSE +0 -0
monoco/daemon/services.py
CHANGED
|
@@ -46,69 +46,25 @@ class Broadcaster:
|
|
|
46
46
|
logger.debug(f"Broadcasted {event_type} to {len(self.subscribers)} clients.")
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
"""
|
|
51
|
-
Polls the Git repository for HEAD changes and triggers updates.
|
|
52
|
-
"""
|
|
53
|
-
def __init__(self, broadcaster: Broadcaster, poll_interval: float = 2.0):
|
|
54
|
-
self.broadcaster = broadcaster
|
|
55
|
-
self.poll_interval = poll_interval
|
|
56
|
-
self.last_head_hash: Optional[str] = None
|
|
57
|
-
self.is_running = False
|
|
58
|
-
|
|
59
|
-
async def get_head_hash(self) -> Optional[str]:
|
|
60
|
-
try:
|
|
61
|
-
# Run git rev-parse HEAD asynchronously
|
|
62
|
-
process = await asyncio.create_subprocess_exec(
|
|
63
|
-
"git", "rev-parse", "HEAD",
|
|
64
|
-
stdout=asyncio.subprocess.PIPE,
|
|
65
|
-
stderr=asyncio.subprocess.PIPE
|
|
66
|
-
)
|
|
67
|
-
stdout, _ = await process.communicate()
|
|
68
|
-
if process.returncode == 0:
|
|
69
|
-
return stdout.decode().strip()
|
|
70
|
-
return None
|
|
71
|
-
except Exception as e:
|
|
72
|
-
logger.error(f"Git polling error: {e}")
|
|
73
|
-
return None
|
|
74
|
-
|
|
75
|
-
async def start(self):
|
|
76
|
-
self.is_running = True
|
|
77
|
-
logger.info("Git Monitor started.")
|
|
78
|
-
|
|
79
|
-
# Initial check
|
|
80
|
-
self.last_head_hash = await self.get_head_hash()
|
|
81
|
-
|
|
82
|
-
while self.is_running:
|
|
83
|
-
await asyncio.sleep(self.poll_interval)
|
|
84
|
-
current_hash = await self.get_head_hash()
|
|
85
|
-
|
|
86
|
-
if current_hash and current_hash != self.last_head_hash:
|
|
87
|
-
logger.info(f"Git HEAD changed: {self.last_head_hash} -> {current_hash}")
|
|
88
|
-
self.last_head_hash = current_hash
|
|
89
|
-
await self.broadcaster.broadcast("HEAD_UPDATED", {
|
|
90
|
-
"ref": "HEAD",
|
|
91
|
-
"hash": current_hash
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
def stop(self):
|
|
95
|
-
self.is_running = False
|
|
96
|
-
logger.info("Git Monitor stopping...")
|
|
49
|
+
# Monitors moved to monoco.core.git and monoco.features.issue.monitor
|
|
97
50
|
|
|
98
51
|
from watchdog.observers import Observer
|
|
99
52
|
from watchdog.events import FileSystemEventHandler
|
|
100
53
|
from monoco.core.config import MonocoConfig, get_config
|
|
101
54
|
|
|
55
|
+
from monoco.core.workspace import MonocoProject, Workspace
|
|
56
|
+
|
|
102
57
|
class ProjectContext:
|
|
103
58
|
"""
|
|
104
59
|
Holds the runtime state for a single project.
|
|
60
|
+
Now wraps the core MonocoProject primitive.
|
|
105
61
|
"""
|
|
106
|
-
def __init__(self,
|
|
107
|
-
self.
|
|
108
|
-
self.
|
|
109
|
-
self.
|
|
110
|
-
self.
|
|
111
|
-
self.issues_root =
|
|
62
|
+
def __init__(self, project: MonocoProject, broadcaster: Broadcaster):
|
|
63
|
+
self.project = project
|
|
64
|
+
self.id = project.id
|
|
65
|
+
self.name = project.name
|
|
66
|
+
self.path = project.path
|
|
67
|
+
self.issues_root = project.issues_root
|
|
112
68
|
self.monitor = IssueMonitor(self.issues_root, broadcaster, project_id=self.id)
|
|
113
69
|
|
|
114
70
|
async def start(self):
|
|
@@ -120,6 +76,7 @@ class ProjectContext:
|
|
|
120
76
|
class ProjectManager:
|
|
121
77
|
"""
|
|
122
78
|
Discovers and manages multiple Monoco projects within a workspace.
|
|
79
|
+
Uses core Workspace primitive for discovery.
|
|
123
80
|
"""
|
|
124
81
|
def __init__(self, workspace_root: Path, broadcaster: Broadcaster):
|
|
125
82
|
self.workspace_root = workspace_root
|
|
@@ -128,28 +85,16 @@ class ProjectManager:
|
|
|
128
85
|
|
|
129
86
|
def scan(self):
|
|
130
87
|
"""
|
|
131
|
-
Scans workspace for
|
|
132
|
-
A directory is a project if it has monoco.yaml or .monoco/config.yaml or an Issues directory.
|
|
88
|
+
Scans workspace for Monoco projects using core logic.
|
|
133
89
|
"""
|
|
134
90
|
logger.info(f"Scanning workspace: {self.workspace_root}")
|
|
135
|
-
|
|
91
|
+
workspace = Workspace.discover(self.workspace_root)
|
|
136
92
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
try:
|
|
143
|
-
config = get_config(str(path))
|
|
144
|
-
# If name is default, try to use directory name
|
|
145
|
-
if config.project.name == "Monoco Project":
|
|
146
|
-
config.project.name = path.name
|
|
147
|
-
|
|
148
|
-
ctx = ProjectContext(path, config, self.broadcaster)
|
|
149
|
-
self.projects[ctx.id] = ctx
|
|
150
|
-
logger.info(f"Registered project: {ctx.id} ({ctx.path})")
|
|
151
|
-
except Exception as e:
|
|
152
|
-
logger.error(f"Failed to register project at {path}: {e}")
|
|
93
|
+
for project in workspace.projects:
|
|
94
|
+
if project.id not in self.projects:
|
|
95
|
+
ctx = ProjectContext(project, self.broadcaster)
|
|
96
|
+
self.projects[ctx.id] = ctx
|
|
97
|
+
logger.info(f"Registered project: {ctx.id} ({ctx.path})")
|
|
153
98
|
|
|
154
99
|
async def start_all(self):
|
|
155
100
|
self.scan()
|
|
@@ -174,92 +119,48 @@ class ProjectManager:
|
|
|
174
119
|
for p in self.projects.values()
|
|
175
120
|
]
|
|
176
121
|
|
|
177
|
-
|
|
178
|
-
def __init__(self, loop, broadcaster: Broadcaster, project_id: str):
|
|
179
|
-
self.loop = loop
|
|
180
|
-
self.broadcaster = broadcaster
|
|
181
|
-
self.project_id = project_id
|
|
182
|
-
|
|
183
|
-
def _process_upsert(self, path_str: str):
|
|
184
|
-
if not path_str.endswith(".md"):
|
|
185
|
-
return
|
|
186
|
-
asyncio.run_coroutine_threadsafe(self._handle_upsert(path_str), self.loop)
|
|
187
|
-
|
|
188
|
-
async def _handle_upsert(self, path_str: str):
|
|
189
|
-
try:
|
|
190
|
-
path = Path(path_str)
|
|
191
|
-
if not path.exists():
|
|
192
|
-
return
|
|
193
|
-
issue = parse_issue(path)
|
|
194
|
-
if issue:
|
|
195
|
-
await self.broadcaster.broadcast("issue_upserted", {
|
|
196
|
-
"issue": issue.model_dump(mode='json'),
|
|
197
|
-
"project_id": self.project_id
|
|
198
|
-
})
|
|
199
|
-
except Exception as e:
|
|
200
|
-
logger.error(f"Error handling upsert for {path_str}: {e}")
|
|
122
|
+
from monoco.features.issue.monitor import IssueMonitor
|
|
201
123
|
|
|
202
|
-
|
|
203
|
-
if not path_str.endswith(".md"):
|
|
204
|
-
return
|
|
205
|
-
asyncio.run_coroutine_threadsafe(self._handle_delete(path_str), self.loop)
|
|
206
|
-
|
|
207
|
-
async def _handle_delete(self, path_str: str):
|
|
208
|
-
try:
|
|
209
|
-
filename = Path(path_str).name
|
|
210
|
-
match = re.match(r"([A-Z]+-\d{4})", filename)
|
|
211
|
-
if match:
|
|
212
|
-
issue_id = match.group(1)
|
|
213
|
-
await self.broadcaster.broadcast("issue_deleted", {
|
|
214
|
-
"id": issue_id,
|
|
215
|
-
"project_id": self.project_id
|
|
216
|
-
})
|
|
217
|
-
except Exception as e:
|
|
218
|
-
logger.error(f"Error handling delete for {path_str}: {e}")
|
|
219
|
-
|
|
220
|
-
def on_created(self, event):
|
|
221
|
-
if not event.is_directory:
|
|
222
|
-
self._process_upsert(event.src_path)
|
|
223
|
-
|
|
224
|
-
def on_modified(self, event):
|
|
225
|
-
if not event.is_directory:
|
|
226
|
-
self._process_upsert(event.src_path)
|
|
227
|
-
|
|
228
|
-
def on_deleted(self, event):
|
|
229
|
-
if not event.is_directory:
|
|
230
|
-
self._process_delete(event.src_path)
|
|
231
|
-
|
|
232
|
-
def on_moved(self, event):
|
|
233
|
-
if not event.is_directory:
|
|
234
|
-
self._process_delete(event.src_path)
|
|
235
|
-
self._process_upsert(event.dest_path)
|
|
236
|
-
|
|
237
|
-
class IssueMonitor:
|
|
124
|
+
class ProjectContext:
|
|
238
125
|
"""
|
|
239
|
-
|
|
126
|
+
Holds the runtime state for a single project.
|
|
127
|
+
Now wraps the core MonocoProject primitive.
|
|
240
128
|
"""
|
|
241
|
-
def __init__(self,
|
|
242
|
-
self.
|
|
243
|
-
self.
|
|
244
|
-
self.
|
|
245
|
-
self.
|
|
246
|
-
self.
|
|
247
|
-
|
|
248
|
-
async def start(self):
|
|
249
|
-
self.loop = asyncio.get_running_loop()
|
|
250
|
-
event_handler = IssueEventHandler(self.loop, self.broadcaster, self.project_id)
|
|
129
|
+
def __init__(self, project: MonocoProject, broadcaster: Broadcaster):
|
|
130
|
+
self.project = project
|
|
131
|
+
self.id = project.id
|
|
132
|
+
self.name = project.name
|
|
133
|
+
self.path = project.path
|
|
134
|
+
self.issues_root = project.issues_root
|
|
251
135
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
136
|
+
async def on_upsert(issue_data: dict):
|
|
137
|
+
await broadcaster.broadcast("issue_upserted", {
|
|
138
|
+
"issue": issue_data,
|
|
139
|
+
"project_id": self.id
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
async def on_delete(issue_data: dict):
|
|
143
|
+
# We skip broadcast here if it's part of a move?
|
|
144
|
+
# Actually, standard upsert/delete is fine, but we need a specialized event for MOVE
|
|
145
|
+
# to help VS Code redirect without closing/reopening.
|
|
146
|
+
await broadcaster.broadcast("issue_deleted", {
|
|
147
|
+
"id": issue_data["id"],
|
|
148
|
+
"project_id": self.id
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
self.monitor = IssueMonitor(self.issues_root, on_upsert, on_delete)
|
|
152
|
+
|
|
153
|
+
async def notify_move(self, old_path: str, new_path: str, issue_data: dict):
|
|
154
|
+
"""Explicitly notify frontend about a logical move (Physical path changed)."""
|
|
155
|
+
await self.broadcaster.broadcast("issue_moved", {
|
|
156
|
+
"old_path": old_path,
|
|
157
|
+
"new_path": new_path,
|
|
158
|
+
"issue": issue_data,
|
|
159
|
+
"project_id": self.id
|
|
160
|
+
})
|
|
256
161
|
|
|
257
|
-
|
|
258
|
-
self.
|
|
259
|
-
logger.info(f"Issue Monitor started (Watchdog). Watching {self.issues_root}")
|
|
162
|
+
async def start(self):
|
|
163
|
+
await self.monitor.start()
|
|
260
164
|
|
|
261
165
|
def stop(self):
|
|
262
|
-
|
|
263
|
-
self.observer.stop()
|
|
264
|
-
self.observer.join()
|
|
265
|
-
logger.info("Issue Monitor stopped.")
|
|
166
|
+
self.monitor.stop()
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
|
|
2
|
+
import typer
|
|
3
|
+
from typing import Optional, Annotated
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from monoco.core.output import print_output, print_error, AgentOutput, OutputManager
|
|
6
|
+
from monoco.core.agent.adapters import get_agent_client
|
|
7
|
+
from monoco.core.agent.state import AgentStateManager
|
|
8
|
+
from monoco.core.agent.action import ActionRegistry, ActionContext
|
|
9
|
+
from monoco.core.config import get_config
|
|
10
|
+
import asyncio
|
|
11
|
+
import re
|
|
12
|
+
import json as j
|
|
13
|
+
|
|
14
|
+
app = typer.Typer()
|
|
15
|
+
|
|
16
|
+
@app.command(name="run")
|
|
17
|
+
def run_command(
|
|
18
|
+
prompt_or_task: str = typer.Argument(..., help="Prompt string OR execution task name (e.g. 'refine-issue')"),
|
|
19
|
+
target: Optional[str] = typer.Argument(None, help="Target file argument for the task"),
|
|
20
|
+
provider: Optional[str] = typer.Option(None, "--using", "-u", help="Override agent provider"),
|
|
21
|
+
instruction: Optional[str] = typer.Option(None, "--instruction", "-i", help="Additional instruction for the agent"),
|
|
22
|
+
json: AgentOutput = False,
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Execute a prompt or a named task using an Agent CLI.
|
|
26
|
+
"""
|
|
27
|
+
# 0. Setup
|
|
28
|
+
settings = get_config()
|
|
29
|
+
state_manager = AgentStateManager()
|
|
30
|
+
registry = ActionRegistry(Path(settings.paths.root))
|
|
31
|
+
|
|
32
|
+
# 1. Check if it's a named task
|
|
33
|
+
action = registry.get(prompt_or_task)
|
|
34
|
+
|
|
35
|
+
final_prompt = prompt_or_task
|
|
36
|
+
context_files = []
|
|
37
|
+
|
|
38
|
+
# Determine Provider Priority: CLI > Action Def > Config > Default
|
|
39
|
+
prov_name = provider
|
|
40
|
+
|
|
41
|
+
if action:
|
|
42
|
+
# It IS an action
|
|
43
|
+
if not OutputManager.is_agent_mode():
|
|
44
|
+
print(f"Running action: {action.name}")
|
|
45
|
+
|
|
46
|
+
# Simple template substitution
|
|
47
|
+
final_prompt = action.template
|
|
48
|
+
|
|
49
|
+
if "{{file}}" in final_prompt:
|
|
50
|
+
if not target:
|
|
51
|
+
print_error("This task requires a target file argument.")
|
|
52
|
+
raise typer.Exit(1)
|
|
53
|
+
|
|
54
|
+
target_path = Path(target).resolve()
|
|
55
|
+
if not target_path.exists():
|
|
56
|
+
print_error(f"Target file not found: {target}")
|
|
57
|
+
raise typer.Exit(1)
|
|
58
|
+
|
|
59
|
+
final_prompt = final_prompt.replace("{{file}}", target_path.read_text())
|
|
60
|
+
# Also add to context files? Ideally the prompt has it.
|
|
61
|
+
# Let's add it to context files list to be safe if prompt didn't embed it fully
|
|
62
|
+
context_files.append(target_path)
|
|
63
|
+
|
|
64
|
+
if not prov_name:
|
|
65
|
+
prov_name = action.provider
|
|
66
|
+
|
|
67
|
+
# 2. Append Instruction if provided
|
|
68
|
+
if instruction:
|
|
69
|
+
final_prompt = f"{final_prompt}\n\n[USER INSTRUCTION]\n{instruction}"
|
|
70
|
+
|
|
71
|
+
# 2. Provider Resolution Fallback
|
|
72
|
+
prov_name = prov_name or settings.agent.framework or "gemini"
|
|
73
|
+
|
|
74
|
+
# 3. State Check
|
|
75
|
+
state = state_manager.load()
|
|
76
|
+
if not state or state.is_stale:
|
|
77
|
+
if not OutputManager.is_agent_mode():
|
|
78
|
+
print("Agent state stale or missing, refreshing...")
|
|
79
|
+
state = state_manager.refresh()
|
|
80
|
+
|
|
81
|
+
if prov_name not in state.providers:
|
|
82
|
+
print_error(f"Provider '{prov_name}' unknown.")
|
|
83
|
+
raise typer.Exit(1)
|
|
84
|
+
|
|
85
|
+
if not state.providers[prov_name].available:
|
|
86
|
+
print_error(f"Provider '{prov_name}' is not available. Run 'monoco doctor' to diagnose.")
|
|
87
|
+
raise typer.Exit(1)
|
|
88
|
+
|
|
89
|
+
# 4. Execute
|
|
90
|
+
try:
|
|
91
|
+
client = get_agent_client(prov_name)
|
|
92
|
+
result = asyncio.run(client.execute(final_prompt, context_files=context_files))
|
|
93
|
+
|
|
94
|
+
if OutputManager.is_agent_mode():
|
|
95
|
+
OutputManager.print({"result": result, "provider": prov_name})
|
|
96
|
+
else:
|
|
97
|
+
print(result)
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
print_error(f"Execution failed: {e}")
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
|
|
103
|
+
@app.command()
|
|
104
|
+
def list(
|
|
105
|
+
json: AgentOutput = False,
|
|
106
|
+
context: Optional[str] = typer.Option(None, "--context", help="Context for filtering (JSON string)")
|
|
107
|
+
):
|
|
108
|
+
"""List available actions."""
|
|
109
|
+
settings = get_config()
|
|
110
|
+
registry = ActionRegistry(Path(settings.paths.root))
|
|
111
|
+
|
|
112
|
+
action_context = None
|
|
113
|
+
if context:
|
|
114
|
+
try:
|
|
115
|
+
ctx_data = j.loads(context)
|
|
116
|
+
action_context = ActionContext(**ctx_data)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
print_error(f"Invalid context JSON: {e}")
|
|
119
|
+
|
|
120
|
+
actions = registry.list_available(action_context)
|
|
121
|
+
# OutputManager handles list of Pydantic models automatically for both JSON and Table
|
|
122
|
+
print_output(actions, title="Available Actions")
|
|
123
|
+
|
|
124
|
+
@app.command()
|
|
125
|
+
def status(
|
|
126
|
+
json: AgentOutput = False,
|
|
127
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force refresh of agent state")
|
|
128
|
+
):
|
|
129
|
+
"""View status of Agent Providers."""
|
|
130
|
+
state_manager = AgentStateManager()
|
|
131
|
+
state = state_manager.get_or_refresh(force=force)
|
|
132
|
+
|
|
133
|
+
if OutputManager.is_agent_mode():
|
|
134
|
+
# Convert datetime to ISO string for JSON serialization
|
|
135
|
+
data = state.dict()
|
|
136
|
+
data["last_checked"] = data["last_checked"].isoformat()
|
|
137
|
+
OutputManager.print(data)
|
|
138
|
+
else:
|
|
139
|
+
# Standard output using existing print_output or custom formatting
|
|
140
|
+
from monoco.core.output import Table
|
|
141
|
+
from rich import print as rprint
|
|
142
|
+
|
|
143
|
+
table = Table(title=f"Agent Status (Last Checked: {state.last_checked.strftime('%Y-%m-%d %H:%M:%S')})")
|
|
144
|
+
table.add_column("Provider")
|
|
145
|
+
table.add_column("Available")
|
|
146
|
+
table.add_column("Path")
|
|
147
|
+
table.add_column("Error")
|
|
148
|
+
|
|
149
|
+
for name, p_state in state.providers.items():
|
|
150
|
+
table.add_row(
|
|
151
|
+
name,
|
|
152
|
+
"✅" if p_state.available else "❌",
|
|
153
|
+
p_state.path or "-",
|
|
154
|
+
p_state.error or "-"
|
|
155
|
+
)
|
|
156
|
+
rprint(table)
|
|
157
|
+
|
|
158
|
+
@app.command()
|
|
159
|
+
def doctor(
|
|
160
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force refresh of agent state")
|
|
161
|
+
):
|
|
162
|
+
"""
|
|
163
|
+
Diagnose Agent Environment and refresh state.
|
|
164
|
+
"""
|
|
165
|
+
from monoco.features.agent.doctor import doctor as doc_impl
|
|
166
|
+
doc_impl(force)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from monoco.core.output import print_output, print_error
|
|
3
|
+
from monoco.core.agent.state import AgentStateManager
|
|
4
|
+
|
|
5
|
+
app = typer.Typer()
|
|
6
|
+
|
|
7
|
+
@app.command()
|
|
8
|
+
def doctor(
|
|
9
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force refresh of agent state")
|
|
10
|
+
):
|
|
11
|
+
"""
|
|
12
|
+
Diagnose Agent Environment and refresh state.
|
|
13
|
+
"""
|
|
14
|
+
manager = AgentStateManager()
|
|
15
|
+
try:
|
|
16
|
+
if force:
|
|
17
|
+
print("Force refreshing agent state...")
|
|
18
|
+
state = manager.refresh()
|
|
19
|
+
else:
|
|
20
|
+
state = manager.get_or_refresh()
|
|
21
|
+
|
|
22
|
+
print_output(state, title="Agent Diagnosis Report")
|
|
23
|
+
|
|
24
|
+
# Simple summary
|
|
25
|
+
available = [k for k, v in state.providers.items() if v.available]
|
|
26
|
+
print(f"\n✅ Available Agents: {', '.join(available) if available else 'None'}")
|
|
27
|
+
|
|
28
|
+
except Exception as e:
|
|
29
|
+
print_error(f"Doctor failed: {e}")
|
|
30
|
+
raise typer.Exit(1)
|
|
@@ -1,70 +1,151 @@
|
|
|
1
1
|
import typer
|
|
2
2
|
import yaml
|
|
3
|
+
import json
|
|
3
4
|
from pathlib import Path
|
|
4
|
-
from typing import Optional
|
|
5
|
-
from monoco.core.config import get_config, MonocoConfig
|
|
6
|
-
from monoco.core.output import print_output
|
|
5
|
+
from typing import Optional, Any, Annotated
|
|
7
6
|
from rich.console import Console
|
|
7
|
+
from rich.syntax import Syntax
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
|
|
10
|
+
from monoco.core.config import (
|
|
11
|
+
get_config,
|
|
12
|
+
MonocoConfig,
|
|
13
|
+
ConfigScope,
|
|
14
|
+
load_raw_config,
|
|
15
|
+
save_raw_config,
|
|
16
|
+
get_config_path
|
|
17
|
+
)
|
|
18
|
+
from monoco.core.output import AgentOutput, OutputManager
|
|
8
19
|
|
|
9
20
|
app = typer.Typer(help="Manage Monoco configuration")
|
|
10
21
|
console = Console()
|
|
11
22
|
|
|
23
|
+
def _parse_value(value: str) -> Any:
|
|
24
|
+
"""Parse string value into appropriate type (bool, int, float, str)."""
|
|
25
|
+
if value.lower() in ("true", "yes", "on"):
|
|
26
|
+
return True
|
|
27
|
+
if value.lower() in ("false", "no", "off"):
|
|
28
|
+
return False
|
|
29
|
+
if value.lower() == "null":
|
|
30
|
+
return None
|
|
31
|
+
try:
|
|
32
|
+
return int(value)
|
|
33
|
+
except ValueError:
|
|
34
|
+
try:
|
|
35
|
+
return float(value)
|
|
36
|
+
except ValueError:
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
@app.command()
|
|
40
|
+
def show(
|
|
41
|
+
output: str = typer.Option("yaml", "--output", "-o", help="Output format: yaml or json"),
|
|
42
|
+
json_output: AgentOutput = False,
|
|
43
|
+
):
|
|
44
|
+
"""Show the currently active (merged) configuration."""
|
|
45
|
+
config = get_config()
|
|
46
|
+
# Pydantic v1/v2 compat: use dict() or model_dump()
|
|
47
|
+
data = config.dict()
|
|
48
|
+
|
|
49
|
+
if OutputManager.is_agent_mode():
|
|
50
|
+
OutputManager.print(data)
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
if output == "json":
|
|
54
|
+
print(json.dumps(data, indent=2))
|
|
55
|
+
else:
|
|
56
|
+
yaml_str = yaml.dump(data, default_flow_style=False)
|
|
57
|
+
syntax = Syntax(yaml_str, "yaml")
|
|
58
|
+
console.print(syntax)
|
|
59
|
+
|
|
12
60
|
@app.command()
|
|
13
|
-
def
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
61
|
+
def get(
|
|
62
|
+
key: str = typer.Argument(..., help="Configuration key (e.g. project.name)"),
|
|
63
|
+
json_output: AgentOutput = False,
|
|
64
|
+
):
|
|
65
|
+
"""Get a specific configuration value."""
|
|
66
|
+
config = get_config()
|
|
67
|
+
data = config.dict()
|
|
68
|
+
|
|
69
|
+
parts = key.split(".")
|
|
70
|
+
current = data
|
|
71
|
+
|
|
72
|
+
for part in parts:
|
|
73
|
+
if isinstance(current, dict) and part in current:
|
|
74
|
+
current = current[part]
|
|
75
|
+
else:
|
|
76
|
+
OutputManager.error(f"Key '{key}' not found.")
|
|
77
|
+
raise typer.Exit(code=1)
|
|
78
|
+
|
|
79
|
+
if OutputManager.is_agent_mode():
|
|
80
|
+
OutputManager.print({"key": key, "value": current})
|
|
81
|
+
else:
|
|
82
|
+
if isinstance(current, (dict, list)):
|
|
83
|
+
if isinstance(current, dict):
|
|
84
|
+
print(yaml.dump(current, default_flow_style=False))
|
|
85
|
+
else:
|
|
86
|
+
print(json.dumps(current))
|
|
87
|
+
else:
|
|
88
|
+
print(current)
|
|
17
89
|
|
|
18
90
|
@app.command(name="set")
|
|
19
91
|
def set_val(
|
|
20
92
|
key: str = typer.Argument(..., help="Config key (e.g. telemetry.enabled)"),
|
|
21
93
|
value: str = typer.Argument(..., help="Value to set"),
|
|
22
|
-
|
|
94
|
+
global_scope: bool = typer.Option(False, "--global", "-g", help="Update global configuration"),
|
|
95
|
+
json_output: AgentOutput = False,
|
|
23
96
|
):
|
|
24
|
-
"""Set a configuration value."""
|
|
25
|
-
|
|
26
|
-
# In a real system, we'd want to validate the key against the schema
|
|
97
|
+
"""Set a configuration value in specific scope (project by default)."""
|
|
98
|
+
scope = ConfigScope.GLOBAL if global_scope else ConfigScope.PROJECT
|
|
27
99
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
cwd = Path.cwd()
|
|
33
|
-
config_path = cwd / ".monoco" / "config.yaml"
|
|
34
|
-
if not (cwd / ".monoco").exists():
|
|
35
|
-
config_path = cwd / "monoco.yaml"
|
|
36
|
-
|
|
37
|
-
config_data = {}
|
|
38
|
-
if config_path.exists():
|
|
39
|
-
with open(config_path, "r") as f:
|
|
40
|
-
config_data = yaml.safe_load(f) or {}
|
|
41
|
-
|
|
42
|
-
# Simple nested key support (e.g. telemetry.enabled)
|
|
100
|
+
# 1. Load Raw Config for the target scope
|
|
101
|
+
raw_data = load_raw_config(scope)
|
|
102
|
+
|
|
103
|
+
# 2. Parse Key & Update Data
|
|
43
104
|
parts = key.split(".")
|
|
44
|
-
target =
|
|
45
|
-
|
|
105
|
+
target = raw_data
|
|
106
|
+
|
|
107
|
+
# Context management for nested updates
|
|
108
|
+
for i, part in enumerate(parts[:-1]):
|
|
46
109
|
if part not in target:
|
|
47
110
|
target[part] = {}
|
|
48
111
|
target = target[part]
|
|
112
|
+
if not isinstance(target, dict):
|
|
113
|
+
parent_key = ".".join(parts[:i+1])
|
|
114
|
+
OutputManager.error(f"Cannot set '{key}': '{parent_key}' is not a dictionary ({type(target)}).")
|
|
115
|
+
raise typer.Exit(code=1)
|
|
116
|
+
|
|
117
|
+
parsed_val = _parse_value(value)
|
|
118
|
+
target[parts[-1]] = parsed_val
|
|
49
119
|
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
120
|
+
# 3. Validate against Schema
|
|
121
|
+
# We simulate a full load by creating a temporary MonocoConfig with these overrides.
|
|
122
|
+
# Note: This validation is "active" - we want to ensure the resulting config WOULD be valid.
|
|
123
|
+
# However, raw_data is partial. Pydantic models with defaults will accept partials.
|
|
124
|
+
try:
|
|
125
|
+
# We can try to validate just the relevant model part if we knew which one it was.
|
|
126
|
+
# But simpler is to check if MonocoConfig accepts this structure.
|
|
127
|
+
MonocoConfig(**raw_data)
|
|
128
|
+
except ValidationError as e:
|
|
129
|
+
OutputManager.error(f"Validation failed for key '{key}':\n{e}")
|
|
130
|
+
raise typer.Exit(code=1)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
OutputManager.error(f"Unexpected validation error: {e}")
|
|
133
|
+
raise typer.Exit(code=1)
|
|
60
134
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
yaml.dump(config_data, f, default_flow_style=False)
|
|
135
|
+
# 4. Save
|
|
136
|
+
save_raw_config(scope, raw_data)
|
|
137
|
+
|
|
138
|
+
scope_display = "Global" if global_scope else "Project"
|
|
66
139
|
|
|
67
|
-
|
|
140
|
+
if OutputManager.is_agent_mode():
|
|
141
|
+
OutputManager.print({
|
|
142
|
+
"status": "updated",
|
|
143
|
+
"scope": scope_display.lower(),
|
|
144
|
+
"key": key,
|
|
145
|
+
"value": parsed_val
|
|
146
|
+
})
|
|
147
|
+
else:
|
|
148
|
+
console.print(f"[green]✓ Set {key} = {parsed_val} in {scope_display} config.[/green]")
|
|
68
149
|
|
|
69
150
|
if __name__ == "__main__":
|
|
70
151
|
app()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Dict
|
|
3
|
+
from monoco.core.feature import MonocoFeature, IntegrationData
|
|
4
|
+
from monoco.features.i18n import core
|
|
5
|
+
|
|
6
|
+
class I18nFeature(MonocoFeature):
|
|
7
|
+
@property
|
|
8
|
+
def name(self) -> str:
|
|
9
|
+
return "i18n"
|
|
10
|
+
|
|
11
|
+
def initialize(self, root: Path, config: Dict) -> None:
|
|
12
|
+
core.init(root)
|
|
13
|
+
|
|
14
|
+
def integrate(self, root: Path, config: Dict) -> IntegrationData:
|
|
15
|
+
# Determine language from config, default to 'en'
|
|
16
|
+
lang = config.get("i18n", {}).get("source_lang", "en")
|
|
17
|
+
base_dir = Path(__file__).parent / "resources"
|
|
18
|
+
|
|
19
|
+
prompt_file = base_dir / lang / "AGENTS.md"
|
|
20
|
+
if not prompt_file.exists():
|
|
21
|
+
prompt_file = base_dir / "en" / "AGENTS.md"
|
|
22
|
+
|
|
23
|
+
content = ""
|
|
24
|
+
if prompt_file.exists():
|
|
25
|
+
content = prompt_file.read_text(encoding="utf-8").strip()
|
|
26
|
+
|
|
27
|
+
return IntegrationData(
|
|
28
|
+
system_prompts={"Documentation I18n": content}
|
|
29
|
+
)
|