monoco-toolkit 0.2.7__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 +500 -260
- monoco/features/issue/core.py +504 -293
- 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 +326 -111
- monoco/features/issue/lsp/definition.py +26 -19
- monoco/features/issue/migration.py +45 -34
- monoco/features/issue/models.py +30 -13
- monoco/features/issue/monitor.py +24 -8
- monoco/features/issue/resources/en/AGENTS.md +5 -0
- monoco/features/issue/resources/en/SKILL.md +30 -2
- monoco/features/issue/resources/zh/AGENTS.md +5 -0
- monoco/features/issue/resources/zh/SKILL.md +26 -1
- monoco/features/issue/validator.py +417 -172
- 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.7.dist-info → monoco_toolkit-0.3.0.dist-info}/METADATA +10 -3
- monoco_toolkit-0.3.0.dist-info/RECORD +84 -0
- monoco_toolkit-0.2.7.dist-info/RECORD +0 -83
- {monoco_toolkit-0.2.7.dist-info → monoco_toolkit-0.3.0.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.2.7.dist-info → monoco_toolkit-0.3.0.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.2.7.dist-info → monoco_toolkit-0.3.0.dist-info}/licenses/LICENSE +0 -0
monoco/daemon/services.py
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
import logging
|
|
3
|
-
import subprocess
|
|
4
|
-
import os
|
|
5
|
-
import re
|
|
6
2
|
from typing import List, Optional, Dict, Any
|
|
7
3
|
from asyncio import Queue
|
|
8
4
|
from pathlib import Path
|
|
9
5
|
|
|
10
|
-
from monoco.features.issue.core import parse_issue, IssueMetadata
|
|
11
6
|
import json
|
|
12
7
|
|
|
13
8
|
logger = logging.getLogger("monoco.daemon.services")
|
|
14
9
|
|
|
10
|
+
|
|
15
11
|
class Broadcaster:
|
|
16
12
|
"""
|
|
17
13
|
Manages SSE subscriptions and broadcasts events to all connected clients.
|
|
18
14
|
"""
|
|
15
|
+
|
|
19
16
|
def __init__(self):
|
|
20
17
|
self.subscribers: List[Queue] = []
|
|
21
18
|
|
|
@@ -33,32 +30,28 @@ class Broadcaster:
|
|
|
33
30
|
async def broadcast(self, event_type: str, payload: dict):
|
|
34
31
|
if not self.subscribers:
|
|
35
32
|
return
|
|
36
|
-
|
|
37
|
-
message = {
|
|
38
|
-
|
|
39
|
-
"data": json.dumps(payload)
|
|
40
|
-
}
|
|
41
|
-
|
|
33
|
+
|
|
34
|
+
message = {"event": event_type, "data": json.dumps(payload)}
|
|
35
|
+
|
|
42
36
|
# Dispatch to all queues
|
|
43
37
|
for queue in self.subscribers:
|
|
44
38
|
await queue.put(message)
|
|
45
|
-
|
|
39
|
+
|
|
46
40
|
logger.debug(f"Broadcasted {event_type} to {len(self.subscribers)} clients.")
|
|
47
41
|
|
|
48
42
|
|
|
49
43
|
# Monitors moved to monoco.core.git and monoco.features.issue.monitor
|
|
50
44
|
|
|
51
|
-
from watchdog.observers import Observer
|
|
52
|
-
from watchdog.events import FileSystemEventHandler
|
|
53
|
-
from monoco.core.config import MonocoConfig, get_config
|
|
54
45
|
|
|
55
46
|
from monoco.core.workspace import MonocoProject, Workspace
|
|
56
47
|
|
|
48
|
+
|
|
57
49
|
class ProjectContext:
|
|
58
50
|
"""
|
|
59
51
|
Holds the runtime state for a single project.
|
|
60
52
|
Now wraps the core MonocoProject primitive.
|
|
61
53
|
"""
|
|
54
|
+
|
|
62
55
|
def __init__(self, project: MonocoProject, broadcaster: Broadcaster):
|
|
63
56
|
self.project = project
|
|
64
57
|
self.id = project.id
|
|
@@ -73,11 +66,13 @@ class ProjectContext:
|
|
|
73
66
|
def stop(self):
|
|
74
67
|
self.monitor.stop()
|
|
75
68
|
|
|
69
|
+
|
|
76
70
|
class ProjectManager:
|
|
77
71
|
"""
|
|
78
72
|
Discovers and manages multiple Monoco projects within a workspace.
|
|
79
73
|
Uses core Workspace primitive for discovery.
|
|
80
74
|
"""
|
|
75
|
+
|
|
81
76
|
def __init__(self, workspace_root: Path, broadcaster: Broadcaster):
|
|
82
77
|
self.workspace_root = workspace_root
|
|
83
78
|
self.broadcaster = broadcaster
|
|
@@ -89,7 +84,7 @@ class ProjectManager:
|
|
|
89
84
|
"""
|
|
90
85
|
logger.info(f"Scanning workspace: {self.workspace_root}")
|
|
91
86
|
workspace = Workspace.discover(self.workspace_root)
|
|
92
|
-
|
|
87
|
+
|
|
93
88
|
for project in workspace.projects:
|
|
94
89
|
if project.id not in self.projects:
|
|
95
90
|
ctx = ProjectContext(project, self.broadcaster)
|
|
@@ -114,50 +109,54 @@ class ProjectManager:
|
|
|
114
109
|
"id": p.id,
|
|
115
110
|
"name": p.name,
|
|
116
111
|
"path": str(p.path),
|
|
117
|
-
"issues_path": str(p.issues_root)
|
|
112
|
+
"issues_path": str(p.issues_root),
|
|
118
113
|
}
|
|
119
114
|
for p in self.projects.values()
|
|
120
115
|
]
|
|
121
116
|
|
|
117
|
+
|
|
122
118
|
from monoco.features.issue.monitor import IssueMonitor
|
|
123
119
|
|
|
120
|
+
|
|
124
121
|
class ProjectContext:
|
|
125
122
|
"""
|
|
126
123
|
Holds the runtime state for a single project.
|
|
127
124
|
Now wraps the core MonocoProject primitive.
|
|
128
125
|
"""
|
|
126
|
+
|
|
129
127
|
def __init__(self, project: MonocoProject, broadcaster: Broadcaster):
|
|
130
128
|
self.project = project
|
|
131
129
|
self.id = project.id
|
|
132
130
|
self.name = project.name
|
|
133
131
|
self.path = project.path
|
|
134
132
|
self.issues_root = project.issues_root
|
|
135
|
-
|
|
133
|
+
|
|
136
134
|
async def on_upsert(issue_data: dict):
|
|
137
|
-
await broadcaster.broadcast(
|
|
138
|
-
"issue": issue_data,
|
|
139
|
-
|
|
140
|
-
})
|
|
135
|
+
await broadcaster.broadcast(
|
|
136
|
+
"issue_upserted", {"issue": issue_data, "project_id": self.id}
|
|
137
|
+
)
|
|
141
138
|
|
|
142
139
|
async def on_delete(issue_data: dict):
|
|
143
140
|
# 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
|
|
141
|
+
# Actually, standard upsert/delete is fine, but we need a specialized event for MOVE
|
|
145
142
|
# to help VS Code redirect without closing/reopening.
|
|
146
|
-
await broadcaster.broadcast(
|
|
147
|
-
"id": issue_data["id"],
|
|
148
|
-
|
|
149
|
-
})
|
|
143
|
+
await broadcaster.broadcast(
|
|
144
|
+
"issue_deleted", {"id": issue_data["id"], "project_id": self.id}
|
|
145
|
+
)
|
|
150
146
|
|
|
151
147
|
self.monitor = IssueMonitor(self.issues_root, on_upsert, on_delete)
|
|
152
148
|
|
|
153
149
|
async def notify_move(self, old_path: str, new_path: str, issue_data: dict):
|
|
154
150
|
"""Explicitly notify frontend about a logical move (Physical path changed)."""
|
|
155
|
-
await self.broadcaster.broadcast(
|
|
156
|
-
"
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
151
|
+
await self.broadcaster.broadcast(
|
|
152
|
+
"issue_moved",
|
|
153
|
+
{
|
|
154
|
+
"old_path": old_path,
|
|
155
|
+
"new_path": new_path,
|
|
156
|
+
"issue": issue_data,
|
|
157
|
+
"project_id": self.id,
|
|
158
|
+
},
|
|
159
|
+
)
|
|
161
160
|
|
|
162
161
|
async def start(self):
|
|
163
162
|
await self.monitor.start()
|
monoco/daemon/stats.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List, Dict,
|
|
1
|
+
from typing import List, Dict, Optional
|
|
2
2
|
from datetime import datetime, timedelta
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from enum import Enum
|
|
@@ -6,11 +6,13 @@ from pydantic import BaseModel
|
|
|
6
6
|
from monoco.features.issue.core import list_issues
|
|
7
7
|
from monoco.features.issue.models import IssueStatus, IssueMetadata
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
class ActivityType(str, Enum):
|
|
10
11
|
CREATED = "created"
|
|
11
12
|
UPDATED = "updated"
|
|
12
13
|
CLOSED = "closed"
|
|
13
14
|
|
|
15
|
+
|
|
14
16
|
class ActivityItem(BaseModel):
|
|
15
17
|
id: str
|
|
16
18
|
type: ActivityType
|
|
@@ -19,30 +21,32 @@ class ActivityItem(BaseModel):
|
|
|
19
21
|
timestamp: datetime
|
|
20
22
|
description: Optional[str] = None
|
|
21
23
|
|
|
24
|
+
|
|
22
25
|
class DashboardStats(BaseModel):
|
|
23
26
|
total_backlog: int
|
|
24
27
|
completed_this_week: int
|
|
25
28
|
blocked_issues_count: int
|
|
26
29
|
velocity_trend: int # Delta compared to last week
|
|
27
30
|
recent_activities: List[ActivityItem] = []
|
|
28
|
-
|
|
31
|
+
|
|
32
|
+
|
|
29
33
|
def calculate_dashboard_stats(issues_root: Path) -> DashboardStats:
|
|
30
34
|
raw_issues = list_issues(issues_root)
|
|
31
|
-
|
|
35
|
+
|
|
32
36
|
# 1. Pre-process for fast lookup and deduplication
|
|
33
37
|
issue_map: Dict[str, IssueMetadata] = {i.id: i for i in raw_issues}
|
|
34
38
|
issues = list(issue_map.values())
|
|
35
|
-
|
|
39
|
+
|
|
36
40
|
backlog_count = 0
|
|
37
41
|
completed_this_week = 0
|
|
38
42
|
completed_last_week = 0
|
|
39
43
|
blocked_count = 0
|
|
40
|
-
|
|
44
|
+
|
|
41
45
|
now = datetime.now()
|
|
42
46
|
one_week_ago = now - timedelta(days=7)
|
|
43
47
|
two_weeks_ago = now - timedelta(days=14)
|
|
44
|
-
activity_window = now - timedelta(days=3)
|
|
45
|
-
|
|
48
|
+
activity_window = now - timedelta(days=3) # Show activities from last 3 days
|
|
49
|
+
|
|
46
50
|
activities: List[ActivityItem] = []
|
|
47
51
|
|
|
48
52
|
for issue in issues:
|
|
@@ -50,7 +54,7 @@ def calculate_dashboard_stats(issues_root: Path) -> DashboardStats:
|
|
|
50
54
|
# Total Backlog
|
|
51
55
|
if issue.status == IssueStatus.BACKLOG:
|
|
52
56
|
backlog_count += 1
|
|
53
|
-
|
|
57
|
+
|
|
54
58
|
# Completed This Week & Last Week
|
|
55
59
|
if issue.status == IssueStatus.CLOSED and issue.closed_at:
|
|
56
60
|
closed_at = issue.closed_at
|
|
@@ -58,7 +62,7 @@ def calculate_dashboard_stats(issues_root: Path) -> DashboardStats:
|
|
|
58
62
|
completed_this_week += 1
|
|
59
63
|
elif closed_at >= two_weeks_ago and closed_at < one_week_ago:
|
|
60
64
|
completed_last_week += 1
|
|
61
|
-
|
|
65
|
+
|
|
62
66
|
# Blocked Issues
|
|
63
67
|
if issue.status == IssueStatus.OPEN:
|
|
64
68
|
is_blocked = False
|
|
@@ -73,52 +77,67 @@ def calculate_dashboard_stats(issues_root: Path) -> DashboardStats:
|
|
|
73
77
|
# --- Activity Feed Generation ---
|
|
74
78
|
# 1. Created Event
|
|
75
79
|
if issue.created_at >= activity_window:
|
|
76
|
-
activities.append(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
activities.append(
|
|
81
|
+
ActivityItem(
|
|
82
|
+
id=f"act_create_{issue.id}",
|
|
83
|
+
type=ActivityType.CREATED,
|
|
84
|
+
issue_id=issue.id,
|
|
85
|
+
issue_title=issue.title,
|
|
86
|
+
timestamp=issue.created_at,
|
|
87
|
+
description="Issue created",
|
|
88
|
+
)
|
|
89
|
+
)
|
|
84
90
|
|
|
85
91
|
# 2. Closed Event
|
|
86
|
-
if
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
if (
|
|
93
|
+
issue.status == IssueStatus.CLOSED
|
|
94
|
+
and issue.closed_at
|
|
95
|
+
and issue.closed_at >= activity_window
|
|
96
|
+
):
|
|
97
|
+
activities.append(
|
|
98
|
+
ActivityItem(
|
|
99
|
+
id=f"act_close_{issue.id}",
|
|
100
|
+
type=ActivityType.CLOSED,
|
|
101
|
+
issue_id=issue.id,
|
|
102
|
+
issue_title=issue.title,
|
|
103
|
+
timestamp=issue.closed_at,
|
|
104
|
+
description="Issue completed",
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
96
108
|
# 3. Updated Event (Heuristic: updated recently and not just created/closed)
|
|
97
109
|
# We skip 'updated' if it's too close to created_at or closed_at to avoid noise
|
|
98
110
|
if issue.updated_at >= activity_window:
|
|
99
|
-
is_creation =
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
is_creation = (
|
|
112
|
+
abs((issue.updated_at - issue.created_at).total_seconds()) < 60
|
|
113
|
+
)
|
|
114
|
+
is_closing = (
|
|
115
|
+
issue.closed_at
|
|
116
|
+
and abs((issue.updated_at - issue.closed_at).total_seconds()) < 60
|
|
117
|
+
)
|
|
118
|
+
|
|
102
119
|
if not is_creation and not is_closing:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
120
|
+
activities.append(
|
|
121
|
+
ActivityItem(
|
|
122
|
+
id=f"act_update_{issue.id}_{issue.updated_at.timestamp()}",
|
|
123
|
+
type=ActivityType.UPDATED,
|
|
124
|
+
issue_id=issue.id,
|
|
125
|
+
issue_title=issue.title,
|
|
126
|
+
timestamp=issue.updated_at,
|
|
127
|
+
description="Issue updated",
|
|
128
|
+
)
|
|
129
|
+
)
|
|
111
130
|
|
|
112
131
|
# Sort activities by timestamp desc and take top 20
|
|
113
132
|
activities.sort(key=lambda x: x.timestamp, reverse=True)
|
|
114
133
|
recent_activities = activities[:20]
|
|
115
134
|
|
|
116
135
|
velocity_trend = completed_this_week - completed_last_week
|
|
117
|
-
|
|
136
|
+
|
|
118
137
|
return DashboardStats(
|
|
119
138
|
total_backlog=backlog_count,
|
|
120
139
|
completed_this_week=completed_this_week,
|
|
121
140
|
blocked_issues_count=blocked_count,
|
|
122
141
|
velocity_trend=velocity_trend,
|
|
123
|
-
recent_activities=recent_activities
|
|
142
|
+
recent_activities=recent_activities,
|
|
124
143
|
)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import typer
|
|
2
2
|
import yaml
|
|
3
3
|
import json
|
|
4
|
-
from
|
|
5
|
-
from typing import Optional, Any, Annotated
|
|
4
|
+
from typing import Any
|
|
6
5
|
from rich.console import Console
|
|
7
6
|
from rich.syntax import Syntax
|
|
8
7
|
from pydantic import ValidationError
|
|
@@ -13,13 +12,13 @@ from monoco.core.config import (
|
|
|
13
12
|
ConfigScope,
|
|
14
13
|
load_raw_config,
|
|
15
14
|
save_raw_config,
|
|
16
|
-
get_config_path
|
|
17
15
|
)
|
|
18
16
|
from monoco.core.output import AgentOutput, OutputManager
|
|
19
17
|
|
|
20
18
|
app = typer.Typer(help="Manage Monoco configuration")
|
|
21
19
|
console = Console()
|
|
22
20
|
|
|
21
|
+
|
|
23
22
|
def _parse_value(value: str) -> Any:
|
|
24
23
|
"""Parse string value into appropriate type (bool, int, float, str)."""
|
|
25
24
|
if value.lower() in ("true", "yes", "on"):
|
|
@@ -36,16 +35,19 @@ def _parse_value(value: str) -> Any:
|
|
|
36
35
|
except ValueError:
|
|
37
36
|
return value
|
|
38
37
|
|
|
38
|
+
|
|
39
39
|
@app.command()
|
|
40
40
|
def show(
|
|
41
|
-
output: str = typer.Option(
|
|
41
|
+
output: str = typer.Option(
|
|
42
|
+
"yaml", "--output", "-o", help="Output format: yaml or json"
|
|
43
|
+
),
|
|
42
44
|
json_output: AgentOutput = False,
|
|
43
45
|
):
|
|
44
46
|
"""Show the currently active (merged) configuration."""
|
|
45
47
|
config = get_config()
|
|
46
48
|
# Pydantic v1/v2 compat: use dict() or model_dump()
|
|
47
49
|
data = config.dict()
|
|
48
|
-
|
|
50
|
+
|
|
49
51
|
if OutputManager.is_agent_mode():
|
|
50
52
|
OutputManager.print(data)
|
|
51
53
|
return
|
|
@@ -57,6 +59,7 @@ def show(
|
|
|
57
59
|
syntax = Syntax(yaml_str, "yaml")
|
|
58
60
|
console.print(syntax)
|
|
59
61
|
|
|
62
|
+
|
|
60
63
|
@app.command()
|
|
61
64
|
def get(
|
|
62
65
|
key: str = typer.Argument(..., help="Configuration key (e.g. project.name)"),
|
|
@@ -65,17 +68,17 @@ def get(
|
|
|
65
68
|
"""Get a specific configuration value."""
|
|
66
69
|
config = get_config()
|
|
67
70
|
data = config.dict()
|
|
68
|
-
|
|
71
|
+
|
|
69
72
|
parts = key.split(".")
|
|
70
73
|
current = data
|
|
71
|
-
|
|
74
|
+
|
|
72
75
|
for part in parts:
|
|
73
76
|
if isinstance(current, dict) and part in current:
|
|
74
77
|
current = current[part]
|
|
75
78
|
else:
|
|
76
79
|
OutputManager.error(f"Key '{key}' not found.")
|
|
77
80
|
raise typer.Exit(code=1)
|
|
78
|
-
|
|
81
|
+
|
|
79
82
|
if OutputManager.is_agent_mode():
|
|
80
83
|
OutputManager.print({"key": key, "value": current})
|
|
81
84
|
else:
|
|
@@ -87,36 +90,41 @@ def get(
|
|
|
87
90
|
else:
|
|
88
91
|
print(current)
|
|
89
92
|
|
|
93
|
+
|
|
90
94
|
@app.command(name="set")
|
|
91
95
|
def set_val(
|
|
92
96
|
key: str = typer.Argument(..., help="Config key (e.g. telemetry.enabled)"),
|
|
93
97
|
value: str = typer.Argument(..., help="Value to set"),
|
|
94
|
-
global_scope: bool = typer.Option(
|
|
98
|
+
global_scope: bool = typer.Option(
|
|
99
|
+
False, "--global", "-g", help="Update global configuration"
|
|
100
|
+
),
|
|
95
101
|
json_output: AgentOutput = False,
|
|
96
102
|
):
|
|
97
103
|
"""Set a configuration value in specific scope (project by default)."""
|
|
98
104
|
scope = ConfigScope.GLOBAL if global_scope else ConfigScope.PROJECT
|
|
99
|
-
|
|
105
|
+
|
|
100
106
|
# 1. Load Raw Config for the target scope
|
|
101
107
|
raw_data = load_raw_config(scope)
|
|
102
|
-
|
|
108
|
+
|
|
103
109
|
# 2. Parse Key & Update Data
|
|
104
110
|
parts = key.split(".")
|
|
105
111
|
target = raw_data
|
|
106
|
-
|
|
112
|
+
|
|
107
113
|
# Context management for nested updates
|
|
108
114
|
for i, part in enumerate(parts[:-1]):
|
|
109
115
|
if part not in target:
|
|
110
116
|
target[part] = {}
|
|
111
117
|
target = target[part]
|
|
112
118
|
if not isinstance(target, dict):
|
|
113
|
-
parent_key = ".".join(parts[:i+1])
|
|
114
|
-
OutputManager.error(
|
|
119
|
+
parent_key = ".".join(parts[: i + 1])
|
|
120
|
+
OutputManager.error(
|
|
121
|
+
f"Cannot set '{key}': '{parent_key}' is not a dictionary ({type(target)})."
|
|
122
|
+
)
|
|
115
123
|
raise typer.Exit(code=1)
|
|
116
|
-
|
|
124
|
+
|
|
117
125
|
parsed_val = _parse_value(value)
|
|
118
126
|
target[parts[-1]] = parsed_val
|
|
119
|
-
|
|
127
|
+
|
|
120
128
|
# 3. Validate against Schema
|
|
121
129
|
# We simulate a full load by creating a temporary MonocoConfig with these overrides.
|
|
122
130
|
# Note: This validation is "active" - we want to ensure the resulting config WOULD be valid.
|
|
@@ -134,18 +142,23 @@ def set_val(
|
|
|
134
142
|
|
|
135
143
|
# 4. Save
|
|
136
144
|
save_raw_config(scope, raw_data)
|
|
137
|
-
|
|
145
|
+
|
|
138
146
|
scope_display = "Global" if global_scope else "Project"
|
|
139
|
-
|
|
147
|
+
|
|
140
148
|
if OutputManager.is_agent_mode():
|
|
141
|
-
OutputManager.print(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
OutputManager.print(
|
|
150
|
+
{
|
|
151
|
+
"status": "updated",
|
|
152
|
+
"scope": scope_display.lower(),
|
|
153
|
+
"key": key,
|
|
154
|
+
"value": parsed_val,
|
|
155
|
+
}
|
|
156
|
+
)
|
|
147
157
|
else:
|
|
148
|
-
console.print(
|
|
158
|
+
console.print(
|
|
159
|
+
f"[green]✓ Set {key} = {parsed_val} in {scope_display} config.[/green]"
|
|
160
|
+
)
|
|
161
|
+
|
|
149
162
|
|
|
150
163
|
if __name__ == "__main__":
|
|
151
164
|
app()
|
monoco/features/i18n/adapter.py
CHANGED
|
@@ -3,6 +3,7 @@ from typing import Dict
|
|
|
3
3
|
from monoco.core.feature import MonocoFeature, IntegrationData
|
|
4
4
|
from monoco.features.i18n import core
|
|
5
5
|
|
|
6
|
+
|
|
6
7
|
class I18nFeature(MonocoFeature):
|
|
7
8
|
@property
|
|
8
9
|
def name(self) -> str:
|
|
@@ -15,15 +16,13 @@ class I18nFeature(MonocoFeature):
|
|
|
15
16
|
# Determine language from config, default to 'en'
|
|
16
17
|
lang = config.get("i18n", {}).get("source_lang", "en")
|
|
17
18
|
base_dir = Path(__file__).parent / "resources"
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
prompt_file = base_dir / lang / "AGENTS.md"
|
|
20
21
|
if not prompt_file.exists():
|
|
21
22
|
prompt_file = base_dir / "en" / "AGENTS.md"
|
|
22
|
-
|
|
23
|
+
|
|
23
24
|
content = ""
|
|
24
25
|
if prompt_file.exists():
|
|
25
26
|
content = prompt_file.read_text(encoding="utf-8").strip()
|
|
26
27
|
|
|
27
|
-
return IntegrationData(
|
|
28
|
-
system_prompts={"Documentation I18n": content}
|
|
29
|
-
)
|
|
28
|
+
return IntegrationData(system_prompts={"Documentation I18n": content})
|