monoco-toolkit 0.1.5__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.5.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.5.dist-info/RECORD +0 -36
- {monoco_toolkit-0.1.5.dist-info → monoco_toolkit-0.1.6.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.1.5.dist-info → monoco_toolkit-0.1.6.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.1.5.dist-info → monoco_toolkit-0.1.6.dist-info}/licenses/LICENSE +0 -0
monoco/core/sync.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
from monoco.core.registry import FeatureRegistry
|
|
5
|
+
from monoco.core.injection import PromptInjector
|
|
6
|
+
from monoco.core.config import get_config
|
|
7
|
+
from monoco.core.skills import SkillManager
|
|
8
|
+
from monoco.core.integrations import get_active_integrations
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
def _get_targets(root: Path, config, cli_target: Optional[Path]) -> List[Path]:
|
|
14
|
+
"""Helper to determine target files."""
|
|
15
|
+
targets = []
|
|
16
|
+
|
|
17
|
+
# 1. CLI Target
|
|
18
|
+
if cli_target:
|
|
19
|
+
targets.append(cli_target)
|
|
20
|
+
return targets
|
|
21
|
+
|
|
22
|
+
# 2. Config Targets
|
|
23
|
+
if config.agent.targets:
|
|
24
|
+
for t in config.agent.targets:
|
|
25
|
+
targets.append(root / t)
|
|
26
|
+
return targets
|
|
27
|
+
|
|
28
|
+
# 3. Registry Defaults (Dynamic Detection)
|
|
29
|
+
integrations = get_active_integrations(
|
|
30
|
+
root,
|
|
31
|
+
config_overrides=config.agent.integrations,
|
|
32
|
+
auto_detect=True
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if integrations:
|
|
36
|
+
for integration in integrations.values():
|
|
37
|
+
targets.append(root / integration.system_prompt_file)
|
|
38
|
+
else:
|
|
39
|
+
# Fallback to standard Monoco header if nothing is detected
|
|
40
|
+
# but we usually want at least one target for a generic sync.
|
|
41
|
+
defaults = ["GEMINI.md", "CLAUDE.md"]
|
|
42
|
+
targets.extend([root / fname for fname in defaults])
|
|
43
|
+
|
|
44
|
+
return list(set(targets)) # Unique paths
|
|
45
|
+
|
|
46
|
+
def sync_command(
|
|
47
|
+
ctx: typer.Context,
|
|
48
|
+
target: Optional[Path] = typer.Option(None, "--target", "-t", help="Specific file to update (default: auto-detect from config or standard files)"),
|
|
49
|
+
check: bool = typer.Option(False, "--check", help="Dry run check mode")
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Synchronize Agent Environment (System Prompts & Skills).
|
|
53
|
+
Aggregates prompts from all active features and injects them into the agent configuration files.
|
|
54
|
+
"""
|
|
55
|
+
root = Path.cwd() # TODO: Use workspace root detection properly if needed
|
|
56
|
+
|
|
57
|
+
# 0. Load Config
|
|
58
|
+
config = get_config(str(root))
|
|
59
|
+
|
|
60
|
+
# 1. Register Features
|
|
61
|
+
registry = FeatureRegistry()
|
|
62
|
+
registry.load_defaults()
|
|
63
|
+
|
|
64
|
+
# 2. Collect Data
|
|
65
|
+
collected_prompts = {}
|
|
66
|
+
|
|
67
|
+
# Filter features based on config if specified
|
|
68
|
+
all_features = registry.get_features()
|
|
69
|
+
active_features = []
|
|
70
|
+
|
|
71
|
+
if config.agent.includes:
|
|
72
|
+
for f in all_features:
|
|
73
|
+
if f.name in config.agent.includes:
|
|
74
|
+
active_features.append(f)
|
|
75
|
+
else:
|
|
76
|
+
active_features = all_features
|
|
77
|
+
|
|
78
|
+
with console.status("[bold green]Collecting feature integration data...") as status:
|
|
79
|
+
for feature in active_features:
|
|
80
|
+
status.update(f"Scanning Feature: {feature.name}")
|
|
81
|
+
try:
|
|
82
|
+
data = feature.integrate(root, config.model_dump())
|
|
83
|
+
if data:
|
|
84
|
+
if data.system_prompts:
|
|
85
|
+
collected_prompts.update(data.system_prompts)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
console.print(f"[red]Error integrating feature {feature.name}: {e}[/red]")
|
|
88
|
+
|
|
89
|
+
console.print(f"[blue]Collected {len(collected_prompts)} prompts from {len(active_features)} features.[/blue]")
|
|
90
|
+
|
|
91
|
+
# 3. Distribute Skills
|
|
92
|
+
console.print(f"[bold blue]Distributing skills to agent frameworks...[/bold blue]")
|
|
93
|
+
|
|
94
|
+
# Determine language from config
|
|
95
|
+
skill_lang = config.i18n.source_lang if config.i18n.source_lang else 'en'
|
|
96
|
+
console.print(f"[dim] Using language: {skill_lang}[/dim]")
|
|
97
|
+
|
|
98
|
+
# Initialize SkillManager with active features
|
|
99
|
+
skill_manager = SkillManager(root, active_features)
|
|
100
|
+
|
|
101
|
+
# Get active integrations
|
|
102
|
+
integrations = get_active_integrations(
|
|
103
|
+
root,
|
|
104
|
+
config_overrides=config.agent.integrations,
|
|
105
|
+
auto_detect=True
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if integrations:
|
|
109
|
+
for framework_key, integration in integrations.items():
|
|
110
|
+
skill_target_dir = root / integration.skill_root_dir
|
|
111
|
+
console.print(f"[dim] Distributing to {integration.name} ({skill_target_dir})...[/dim]")
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
# Distribute only the configured language version
|
|
115
|
+
results = skill_manager.distribute(skill_target_dir, lang=skill_lang, force=False)
|
|
116
|
+
success_count = sum(1 for v in results.values() if v)
|
|
117
|
+
console.print(f"[green] ✓ Distributed {success_count}/{len(results)} skills to {integration.name}[/green]")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
console.print(f"[red] Failed to distribute skills to {integration.name}: {e}[/red]")
|
|
120
|
+
else:
|
|
121
|
+
console.print(f"[yellow]No agent frameworks detected. Skipping skill distribution.[/yellow]")
|
|
122
|
+
|
|
123
|
+
# 4. Determine Targets
|
|
124
|
+
targets = _get_targets(root, config, target)
|
|
125
|
+
|
|
126
|
+
# Ensure targets exist for sync
|
|
127
|
+
final_targets = []
|
|
128
|
+
for t in targets:
|
|
129
|
+
if not t.exists():
|
|
130
|
+
# If explicit target, fail? Or create?
|
|
131
|
+
# If default, create.
|
|
132
|
+
if target:
|
|
133
|
+
# CLI target
|
|
134
|
+
console.print(f"[yellow]Creating {t.name}...[/yellow]")
|
|
135
|
+
try:
|
|
136
|
+
t.touch()
|
|
137
|
+
final_targets.append(t)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
console.print(f"[red]Failed to create {t}: {e}[/red]")
|
|
140
|
+
else:
|
|
141
|
+
# Default/Config target -> only create if it's one of the defaults we manage?
|
|
142
|
+
# For now, let's just create it to be safe, assuming user wants it.
|
|
143
|
+
console.print(f"[yellow]Creating {t.name}...[/yellow]")
|
|
144
|
+
try:
|
|
145
|
+
t.touch()
|
|
146
|
+
final_targets.append(t)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
console.print(f"[red]Failed to create {t}: {e}[/red]")
|
|
149
|
+
else:
|
|
150
|
+
final_targets.append(t)
|
|
151
|
+
|
|
152
|
+
# 5. Inject System Prompts
|
|
153
|
+
for t in final_targets:
|
|
154
|
+
injector = PromptInjector(t)
|
|
155
|
+
|
|
156
|
+
if check:
|
|
157
|
+
console.print(f"[dim][Dry Run] Would check/update {t.name}[/dim]")
|
|
158
|
+
else:
|
|
159
|
+
try:
|
|
160
|
+
changed = injector.inject(collected_prompts)
|
|
161
|
+
if changed:
|
|
162
|
+
console.print(f"[green]✓ Updated {t.name}[/green]")
|
|
163
|
+
else:
|
|
164
|
+
console.print(f"[dim]= {t.name} is up to date[/dim]")
|
|
165
|
+
except Exception as e:
|
|
166
|
+
console.print(f"[red]Failed to update {t.name}: {e}[/red]")
|
|
167
|
+
|
|
168
|
+
def uninstall_command(
|
|
169
|
+
ctx: typer.Context,
|
|
170
|
+
target: Optional[Path] = typer.Option(None, "--target", "-t", help="Specific file to clean (default: auto-detect from config or standard files)")
|
|
171
|
+
):
|
|
172
|
+
"""
|
|
173
|
+
Remove Monoco Managed Block from Agent Environment files and clean up distributed skills.
|
|
174
|
+
"""
|
|
175
|
+
root = Path.cwd()
|
|
176
|
+
config = get_config(str(root))
|
|
177
|
+
|
|
178
|
+
# 1. Clean up System Prompts
|
|
179
|
+
targets = _get_targets(root, config, target)
|
|
180
|
+
|
|
181
|
+
for t in targets:
|
|
182
|
+
if not t.exists():
|
|
183
|
+
if target:
|
|
184
|
+
console.print(f"[yellow]Target {t} does not exist.[/yellow]")
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
injector = PromptInjector(t)
|
|
188
|
+
try:
|
|
189
|
+
changed = injector.remove()
|
|
190
|
+
if changed:
|
|
191
|
+
console.print(f"[green]✓ Removed Monoco Managed Block from {t.name}[/green]")
|
|
192
|
+
else:
|
|
193
|
+
console.print(f"[dim]= No Monoco Block found in {t.name}[/dim]")
|
|
194
|
+
except Exception as e:
|
|
195
|
+
console.print(f"[red]Failed to uninstall from {t.name}: {e}[/red]")
|
|
196
|
+
|
|
197
|
+
# 2. Clean up Skills
|
|
198
|
+
console.print(f"[bold blue]Cleaning up distributed skills...[/bold blue]")
|
|
199
|
+
|
|
200
|
+
# Load features to get skill list
|
|
201
|
+
registry = FeatureRegistry()
|
|
202
|
+
registry.load_defaults()
|
|
203
|
+
active_features = registry.get_features()
|
|
204
|
+
|
|
205
|
+
skill_manager = SkillManager(root, active_features)
|
|
206
|
+
|
|
207
|
+
# Get active integrations
|
|
208
|
+
integrations = get_active_integrations(
|
|
209
|
+
root,
|
|
210
|
+
config_overrides=config.agent.integrations,
|
|
211
|
+
auto_detect=True
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if integrations:
|
|
215
|
+
for framework_key, integration in integrations.items():
|
|
216
|
+
skill_target_dir = root / integration.skill_root_dir
|
|
217
|
+
console.print(f"[dim] Cleaning {integration.name} ({skill_target_dir})...[/dim]")
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
skill_manager.cleanup(skill_target_dir)
|
|
221
|
+
except Exception as e:
|
|
222
|
+
console.print(f"[red] Failed to clean skills from {integration.name}: {e}[/red]")
|
|
223
|
+
else:
|
|
224
|
+
console.print(f"[yellow]No agent frameworks detected. Skipping skill cleanup.[/yellow]")
|
monoco/core/workspace.py
CHANGED
|
@@ -5,16 +5,12 @@ def is_project_root(path: Path) -> bool:
|
|
|
5
5
|
"""
|
|
6
6
|
Check if a directory serves as a Monoco project root.
|
|
7
7
|
Criteria:
|
|
8
|
-
- has monoco
|
|
9
|
-
- OR has .monoco/config.yaml
|
|
10
|
-
- OR has Issues/ directory
|
|
8
|
+
- has .monoco/ directory
|
|
11
9
|
"""
|
|
12
10
|
if not path.is_dir():
|
|
13
11
|
return False
|
|
14
12
|
|
|
15
|
-
return (path / "monoco
|
|
16
|
-
(path / ".monoco" / "config.yaml").exists() or \
|
|
17
|
-
(path / "Issues").exists()
|
|
13
|
+
return (path / ".monoco").is_dir()
|
|
18
14
|
|
|
19
15
|
def find_projects(workspace_root: Path) -> List[Path]:
|
|
20
16
|
"""
|
monoco/daemon/services.py
CHANGED
|
@@ -129,7 +129,7 @@ class ProjectManager:
|
|
|
129
129
|
def scan(self):
|
|
130
130
|
"""
|
|
131
131
|
Scans workspace for potential Monoco projects.
|
|
132
|
-
A directory is a project if it has
|
|
132
|
+
A directory is a project if it has a .monoco/ directory.
|
|
133
133
|
"""
|
|
134
134
|
logger.info(f"Scanning workspace: {self.workspace_root}")
|
|
135
135
|
from monoco.core.workspace import find_projects
|
|
@@ -1,70 +1,130 @@
|
|
|
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
|
|
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
|
+
)
|
|
8
18
|
|
|
9
19
|
app = typer.Typer(help="Manage Monoco configuration")
|
|
10
20
|
console = Console()
|
|
11
21
|
|
|
22
|
+
def _parse_value(value: str) -> Any:
|
|
23
|
+
"""Parse string value into appropriate type (bool, int, float, str)."""
|
|
24
|
+
if value.lower() in ("true", "yes", "on"):
|
|
25
|
+
return True
|
|
26
|
+
if value.lower() in ("false", "no", "off"):
|
|
27
|
+
return False
|
|
28
|
+
if value.lower() == "null":
|
|
29
|
+
return None
|
|
30
|
+
try:
|
|
31
|
+
return int(value)
|
|
32
|
+
except ValueError:
|
|
33
|
+
try:
|
|
34
|
+
return float(value)
|
|
35
|
+
except ValueError:
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
@app.command()
|
|
39
|
+
def show(
|
|
40
|
+
output: str = typer.Option("yaml", "--output", "-o", help="Output format: yaml or json"),
|
|
41
|
+
):
|
|
42
|
+
"""Show the currently active (merged) configuration."""
|
|
43
|
+
config = get_config()
|
|
44
|
+
# Pydantic v1/v2 compat: use dict() or model_dump()
|
|
45
|
+
data = config.dict()
|
|
46
|
+
|
|
47
|
+
if output == "json":
|
|
48
|
+
print(json.dumps(data, indent=2))
|
|
49
|
+
else:
|
|
50
|
+
yaml_str = yaml.dump(data, default_flow_style=False)
|
|
51
|
+
syntax = Syntax(yaml_str, "yaml")
|
|
52
|
+
console.print(syntax)
|
|
53
|
+
|
|
12
54
|
@app.command()
|
|
13
|
-
def
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
|
|
55
|
+
def get(key: str = typer.Argument(..., help="Configuration key (e.g. project.name)")):
|
|
56
|
+
"""Get a specific configuration value."""
|
|
57
|
+
config = get_config()
|
|
58
|
+
data = config.dict()
|
|
59
|
+
|
|
60
|
+
parts = key.split(".")
|
|
61
|
+
current = data
|
|
62
|
+
|
|
63
|
+
for part in parts:
|
|
64
|
+
if isinstance(current, dict) and part in current:
|
|
65
|
+
current = current[part]
|
|
66
|
+
else:
|
|
67
|
+
console.print(f"[red]Key '{key}' not found.[/red]")
|
|
68
|
+
raise typer.Exit(code=1)
|
|
69
|
+
|
|
70
|
+
if isinstance(current, (dict, list)):
|
|
71
|
+
if isinstance(current, dict):
|
|
72
|
+
print(yaml.dump(current, default_flow_style=False))
|
|
73
|
+
else:
|
|
74
|
+
print(json.dumps(current))
|
|
75
|
+
else:
|
|
76
|
+
print(current)
|
|
17
77
|
|
|
18
78
|
@app.command(name="set")
|
|
19
79
|
def set_val(
|
|
20
80
|
key: str = typer.Argument(..., help="Config key (e.g. telemetry.enabled)"),
|
|
21
81
|
value: str = typer.Argument(..., help="Value to set"),
|
|
22
|
-
|
|
82
|
+
global_scope: bool = typer.Option(False, "--global", "-g", help="Update global configuration"),
|
|
23
83
|
):
|
|
24
|
-
"""Set a configuration value."""
|
|
25
|
-
|
|
26
|
-
# In a real system, we'd want to validate the key against the schema
|
|
84
|
+
"""Set a configuration value in specific scope (project by default)."""
|
|
85
|
+
scope = ConfigScope.GLOBAL if global_scope else ConfigScope.PROJECT
|
|
27
86
|
|
|
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)
|
|
87
|
+
# 1. Load Raw Config for the target scope
|
|
88
|
+
raw_data = load_raw_config(scope)
|
|
89
|
+
|
|
90
|
+
# 2. Parse Key & Update Data
|
|
43
91
|
parts = key.split(".")
|
|
44
|
-
target =
|
|
45
|
-
|
|
92
|
+
target = raw_data
|
|
93
|
+
|
|
94
|
+
# Context management for nested updates
|
|
95
|
+
for i, part in enumerate(parts[:-1]):
|
|
46
96
|
if part not in target:
|
|
47
97
|
target[part] = {}
|
|
48
98
|
target = target[part]
|
|
99
|
+
if not isinstance(target, dict):
|
|
100
|
+
parent_key = ".".join(parts[:i+1])
|
|
101
|
+
console.print(f"[red]Cannot set '{key}': '{parent_key}' is not a dictionary ({type(target)}).[/red]")
|
|
102
|
+
raise typer.Exit(code=1)
|
|
103
|
+
|
|
104
|
+
parsed_val = _parse_value(value)
|
|
105
|
+
target[parts[-1]] = parsed_val
|
|
49
106
|
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
107
|
+
# 3. Validate against Schema
|
|
108
|
+
# We simulate a full load by creating a temporary MonocoConfig with these overrides.
|
|
109
|
+
# Note: This validation is "active" - we want to ensure the resulting config WOULD be valid.
|
|
110
|
+
# However, raw_data is partial. Pydantic models with defaults will accept partials.
|
|
111
|
+
try:
|
|
112
|
+
# We can try to validate just the relevant model part if we knew which one it was.
|
|
113
|
+
# But simpler is to check if MonocoConfig accepts this structure.
|
|
114
|
+
MonocoConfig(**raw_data)
|
|
115
|
+
except ValidationError as e:
|
|
116
|
+
console.print(f"[red]Validation failed for key '{key}':[/red]")
|
|
117
|
+
console.print(e)
|
|
118
|
+
raise typer.Exit(code=1)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
console.print(f"[red]Unexpected validation error: {e}[/red]")
|
|
121
|
+
raise typer.Exit(code=1)
|
|
62
122
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
yaml.dump(config_data, f, default_flow_style=False)
|
|
123
|
+
# 4. Save
|
|
124
|
+
save_raw_config(scope, raw_data)
|
|
66
125
|
|
|
67
|
-
|
|
126
|
+
scope_display = "Global" if global_scope else "Project"
|
|
127
|
+
console.print(f"[green]✓ Set {key} = {parsed_val} in {scope_display} config.[/green]")
|
|
68
128
|
|
|
69
129
|
if __name__ == "__main__":
|
|
70
130
|
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
|
+
)
|
monoco/features/i18n/core.py
CHANGED
|
@@ -154,25 +154,15 @@ i18n is a "first-class citizen" in Monoco.
|
|
|
154
154
|
- Run `monoco i18n scan` to verify coverage.
|
|
155
155
|
"""
|
|
156
156
|
|
|
157
|
-
PROMPT_CONTENT = """### Documentation I18n
|
|
158
|
-
Manage internationalization.
|
|
159
|
-
- **Scan**: `monoco i18n scan` (Check for missing translations)
|
|
160
|
-
- **Structure**:
|
|
161
|
-
- Root files: `FILE_ZH.md`
|
|
162
|
-
- Subdirs: `folder/zh/file.md`"""
|
|
163
157
|
|
|
164
158
|
def init(root: Path):
|
|
165
159
|
"""Initialize I18n environment (No-op currently as it relies on config)."""
|
|
166
160
|
# In future, could generate i18n config section if missing.
|
|
167
161
|
pass
|
|
168
162
|
|
|
169
|
-
def get_resources() -> Dict[str, Any]:
|
|
170
163
|
return {
|
|
171
164
|
"skills": {
|
|
172
165
|
"i18n": SKILL_CONTENT
|
|
173
166
|
},
|
|
174
|
-
"prompts": {
|
|
175
|
-
"i18n": PROMPT_CONTENT
|
|
176
|
-
}
|
|
167
|
+
"prompts": {} # Handled by adapter via resource files
|
|
177
168
|
}
|
|
178
|
-
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: monoco-i18n
|
|
3
|
+
description: Internationalization quality control for documentation. Ensures multi-language documentation stays synchronized.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Documentation I18n
|
|
7
|
+
|
|
8
|
+
Manage internationalization for Monoco project documentation.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
The I18n feature provides:
|
|
13
|
+
|
|
14
|
+
- **Automatic scanning** for missing translations
|
|
15
|
+
- **Standardized structure** for multi-language documentation
|
|
16
|
+
- **Quality control** to maintain documentation parity
|
|
17
|
+
|
|
18
|
+
## Key Commands
|
|
19
|
+
|
|
20
|
+
### Scan for Missing Translations
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
monoco i18n scan
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Scans the project for markdown files and reports missing translations.
|
|
27
|
+
|
|
28
|
+
**Output**:
|
|
29
|
+
|
|
30
|
+
- Lists source files without corresponding translations
|
|
31
|
+
- Shows which target languages are missing
|
|
32
|
+
- Respects `.gitignore` and default exclusions
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
I18n settings are configured in `.monoco/config.yaml`:
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
i18n:
|
|
40
|
+
source_lang: en # Source language code
|
|
41
|
+
target_langs: # Target language codes
|
|
42
|
+
- zh
|
|
43
|
+
- ja
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Documentation Structure
|
|
47
|
+
|
|
48
|
+
### Root Files (Suffix Pattern)
|
|
49
|
+
|
|
50
|
+
For files in the project root:
|
|
51
|
+
|
|
52
|
+
- Source: `README.md`
|
|
53
|
+
- Chinese: `README_ZH.md`
|
|
54
|
+
- Japanese: `README_JA.md`
|
|
55
|
+
|
|
56
|
+
### Subdirectory Files (Directory Pattern)
|
|
57
|
+
|
|
58
|
+
For files in `docs/` or other directories:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
docs/
|
|
62
|
+
├── en/
|
|
63
|
+
│ ├── guide.md
|
|
64
|
+
│ └── api.md
|
|
65
|
+
├── zh/
|
|
66
|
+
│ ├── guide.md
|
|
67
|
+
│ └── api.md
|
|
68
|
+
└── ja/
|
|
69
|
+
├── guide.md
|
|
70
|
+
└── api.md
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Exclusion Rules
|
|
74
|
+
|
|
75
|
+
The following are automatically excluded from i18n scanning:
|
|
76
|
+
|
|
77
|
+
- `.gitignore` patterns (respected automatically)
|
|
78
|
+
- `.references/` directory
|
|
79
|
+
- Build artifacts (`dist/`, `build/`, `node_modules/`)
|
|
80
|
+
- `Issues/` directory
|
|
81
|
+
|
|
82
|
+
## Best Practices
|
|
83
|
+
|
|
84
|
+
1. **Create English First**: Write documentation in the source language first
|
|
85
|
+
2. **Follow Naming Convention**: Use the appropriate pattern (suffix or directory)
|
|
86
|
+
3. **Run Scan Regularly**: Use `monoco i18n scan` to verify coverage
|
|
87
|
+
4. **Commit All Languages**: Keep translations in version control
|
|
88
|
+
|
|
89
|
+
## Workflow
|
|
90
|
+
|
|
91
|
+
1. Write documentation in source language (e.g., English)
|
|
92
|
+
2. Create translation files following the naming convention
|
|
93
|
+
3. Run `monoco i18n scan` to verify all translations exist
|
|
94
|
+
4. Fix any missing translations reported by the scan
|