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/core/skills.py
CHANGED
|
@@ -14,7 +14,7 @@ Key Responsibilities:
|
|
|
14
14
|
import shutil
|
|
15
15
|
import hashlib
|
|
16
16
|
from pathlib import Path
|
|
17
|
-
from typing import Dict, List, Optional
|
|
17
|
+
from typing import Dict, List, Optional
|
|
18
18
|
from pydantic import BaseModel, Field, ValidationError
|
|
19
19
|
from rich.console import Console
|
|
20
20
|
import yaml
|
|
@@ -27,21 +27,27 @@ class SkillMetadata(BaseModel):
|
|
|
27
27
|
Skill metadata from YAML frontmatter.
|
|
28
28
|
Based on agentskills.io standard.
|
|
29
29
|
"""
|
|
30
|
+
|
|
30
31
|
name: str = Field(..., description="Unique skill identifier (lowercase, hyphens)")
|
|
31
|
-
description: str = Field(
|
|
32
|
+
description: str = Field(
|
|
33
|
+
..., description="Clear description of what the skill does and when to use it"
|
|
34
|
+
)
|
|
32
35
|
version: Optional[str] = Field(default=None, description="Skill version")
|
|
33
36
|
author: Optional[str] = Field(default=None, description="Skill author")
|
|
34
|
-
tags: Optional[List[str]] = Field(
|
|
37
|
+
tags: Optional[List[str]] = Field(
|
|
38
|
+
default=None, description="Skill tags for categorization"
|
|
39
|
+
)
|
|
35
40
|
|
|
36
41
|
|
|
37
42
|
class Skill:
|
|
38
43
|
"""
|
|
39
44
|
Represents a single skill with its metadata and file paths.
|
|
40
45
|
"""
|
|
46
|
+
|
|
41
47
|
def __init__(self, root_dir: Path, skill_dir: Path):
|
|
42
48
|
"""
|
|
43
49
|
Initialize a Skill instance.
|
|
44
|
-
|
|
50
|
+
|
|
45
51
|
Args:
|
|
46
52
|
root_dir: Project root directory
|
|
47
53
|
skill_dir: Path to the skill directory (e.g., Toolkit/skills/issues-management)
|
|
@@ -52,13 +58,13 @@ class Skill:
|
|
|
52
58
|
self.skill_file = skill_dir / "SKILL.md"
|
|
53
59
|
self.metadata: Optional[SkillMetadata] = None
|
|
54
60
|
self._load_metadata()
|
|
55
|
-
|
|
61
|
+
|
|
56
62
|
def _load_metadata(self) -> None:
|
|
57
63
|
"""Load and validate skill metadata from SKILL.md frontmatter."""
|
|
58
64
|
# Try to load from language subdirectories first (Feature resources pattern)
|
|
59
65
|
# Then fallback to root SKILL.md (legacy pattern)
|
|
60
66
|
skill_file_to_use = None
|
|
61
|
-
|
|
67
|
+
|
|
62
68
|
# Check language subdirectories
|
|
63
69
|
if self.skill_dir.exists():
|
|
64
70
|
for item in sorted(self.skill_dir.iterdir()):
|
|
@@ -67,14 +73,14 @@ class Skill:
|
|
|
67
73
|
if candidate.exists():
|
|
68
74
|
skill_file_to_use = candidate
|
|
69
75
|
break
|
|
70
|
-
|
|
76
|
+
|
|
71
77
|
# Fallback to root SKILL.md
|
|
72
78
|
if not skill_file_to_use and self.skill_file.exists():
|
|
73
79
|
skill_file_to_use = self.skill_file
|
|
74
|
-
|
|
80
|
+
|
|
75
81
|
if not skill_file_to_use:
|
|
76
82
|
return
|
|
77
|
-
|
|
83
|
+
|
|
78
84
|
try:
|
|
79
85
|
content = skill_file_to_use.read_text(encoding="utf-8")
|
|
80
86
|
# Extract YAML frontmatter
|
|
@@ -83,27 +89,29 @@ class Skill:
|
|
|
83
89
|
if len(parts) >= 3:
|
|
84
90
|
frontmatter = parts[1].strip()
|
|
85
91
|
metadata_dict = yaml.safe_load(frontmatter)
|
|
86
|
-
|
|
92
|
+
|
|
87
93
|
# Validate against schema
|
|
88
94
|
self.metadata = SkillMetadata(**metadata_dict)
|
|
89
95
|
except ValidationError as e:
|
|
90
96
|
console.print(f"[red]Invalid metadata in {skill_file_to_use}: {e}[/red]")
|
|
91
97
|
except Exception as e:
|
|
92
|
-
console.print(
|
|
93
|
-
|
|
98
|
+
console.print(
|
|
99
|
+
f"[yellow]Warning: Failed to parse metadata from {skill_file_to_use}: {e}[/yellow]"
|
|
100
|
+
)
|
|
101
|
+
|
|
94
102
|
def is_valid(self) -> bool:
|
|
95
103
|
"""Check if the skill has valid metadata."""
|
|
96
104
|
return self.metadata is not None
|
|
97
|
-
|
|
105
|
+
|
|
98
106
|
def get_languages(self) -> List[str]:
|
|
99
107
|
"""
|
|
100
108
|
Detect available language versions of this skill.
|
|
101
|
-
|
|
109
|
+
|
|
102
110
|
Returns:
|
|
103
111
|
List of language codes (e.g., ['en', 'zh'])
|
|
104
112
|
"""
|
|
105
113
|
languages = []
|
|
106
|
-
|
|
114
|
+
|
|
107
115
|
# Check for language subdirectories (Feature resources pattern)
|
|
108
116
|
# resources/en/SKILL.md, resources/zh/SKILL.md
|
|
109
117
|
for item in self.skill_dir.iterdir():
|
|
@@ -111,36 +119,36 @@ class Skill:
|
|
|
111
119
|
lang_skill_file = item / "SKILL.md"
|
|
112
120
|
if lang_skill_file.exists():
|
|
113
121
|
languages.append(item.name)
|
|
114
|
-
|
|
122
|
+
|
|
115
123
|
# Fallback: check for root SKILL.md (legacy Toolkit/skills pattern)
|
|
116
124
|
# We don't assume a default language, just return what we found
|
|
117
125
|
if not languages and self.skill_file.exists():
|
|
118
126
|
# For legacy pattern, we can't determine the language from structure
|
|
119
127
|
# Return empty to indicate this skill uses legacy pattern
|
|
120
128
|
pass
|
|
121
|
-
|
|
129
|
+
|
|
122
130
|
return languages
|
|
123
|
-
|
|
131
|
+
|
|
124
132
|
def get_checksum(self, lang: str) -> str:
|
|
125
133
|
"""
|
|
126
134
|
Calculate checksum for the skill content.
|
|
127
|
-
|
|
135
|
+
|
|
128
136
|
Args:
|
|
129
137
|
lang: Language code
|
|
130
|
-
|
|
138
|
+
|
|
131
139
|
Returns:
|
|
132
140
|
SHA256 checksum of the skill file
|
|
133
141
|
"""
|
|
134
142
|
# Try language subdirectory first (Feature resources pattern)
|
|
135
143
|
target_file = self.skill_dir / lang / "SKILL.md"
|
|
136
|
-
|
|
144
|
+
|
|
137
145
|
# Fallback to root SKILL.md (legacy pattern)
|
|
138
146
|
if not target_file.exists():
|
|
139
147
|
target_file = self.skill_file
|
|
140
|
-
|
|
148
|
+
|
|
141
149
|
if not target_file.exists():
|
|
142
150
|
return ""
|
|
143
|
-
|
|
151
|
+
|
|
144
152
|
content = target_file.read_bytes()
|
|
145
153
|
return hashlib.sha256(content).hexdigest()
|
|
146
154
|
|
|
@@ -148,17 +156,17 @@ class Skill:
|
|
|
148
156
|
class SkillManager:
|
|
149
157
|
"""
|
|
150
158
|
Central manager for Monoco skills.
|
|
151
|
-
|
|
159
|
+
|
|
152
160
|
Responsibilities:
|
|
153
161
|
- Collect skills from Feature resources
|
|
154
162
|
- Validate skill structure
|
|
155
163
|
- Distribute skills to agent framework directories
|
|
156
164
|
"""
|
|
157
|
-
|
|
165
|
+
|
|
158
166
|
def __init__(self, root: Path, features: Optional[List] = None):
|
|
159
167
|
"""
|
|
160
168
|
Initialize SkillManager.
|
|
161
|
-
|
|
169
|
+
|
|
162
170
|
Args:
|
|
163
171
|
root: Project root directory
|
|
164
172
|
features: List of MonocoFeature instances (if None, will load from registry)
|
|
@@ -166,178 +174,183 @@ class SkillManager:
|
|
|
166
174
|
self.root = root
|
|
167
175
|
self.features = features or []
|
|
168
176
|
self.skills: Dict[str, Skill] = {}
|
|
169
|
-
|
|
177
|
+
|
|
170
178
|
if self.features:
|
|
171
179
|
self._discover_skills_from_features()
|
|
172
|
-
|
|
180
|
+
|
|
173
181
|
# Also discover core skill (monoco/core/resources/)
|
|
174
182
|
self._discover_core_skill()
|
|
175
|
-
|
|
183
|
+
|
|
176
184
|
def _discover_core_skill(self) -> None:
|
|
177
185
|
"""
|
|
178
186
|
Discover skill from monoco/core/resources/.
|
|
179
|
-
|
|
187
|
+
|
|
180
188
|
Core is special - it's not a Feature but still has a skill.
|
|
181
189
|
"""
|
|
182
190
|
core_resources_dir = self.root / "monoco" / "core" / "resources"
|
|
183
|
-
|
|
191
|
+
|
|
184
192
|
if not core_resources_dir.exists():
|
|
185
193
|
return
|
|
186
|
-
|
|
194
|
+
|
|
187
195
|
# Check for SKILL.md in language directories
|
|
188
196
|
for lang_dir in core_resources_dir.iterdir():
|
|
189
197
|
if lang_dir.is_dir() and (lang_dir / "SKILL.md").exists():
|
|
190
198
|
skill = Skill(self.root, core_resources_dir)
|
|
191
|
-
|
|
199
|
+
|
|
192
200
|
# Use the skill's metadata name if available
|
|
193
201
|
if skill.metadata and skill.metadata.name:
|
|
194
|
-
skill.name = skill.metadata.name.replace(
|
|
202
|
+
skill.name = skill.metadata.name.replace("-", "_")
|
|
195
203
|
else:
|
|
196
204
|
skill.name = "monoco_core"
|
|
197
|
-
|
|
205
|
+
|
|
198
206
|
if skill.is_valid():
|
|
199
207
|
self.skills[skill.name] = skill
|
|
200
208
|
break # Only need to detect once
|
|
201
|
-
|
|
209
|
+
|
|
202
210
|
def _discover_skills_from_features(self) -> None:
|
|
203
211
|
"""
|
|
204
212
|
Discover skills from Feature resources.
|
|
205
|
-
|
|
213
|
+
|
|
206
214
|
Each feature should have:
|
|
207
215
|
- monoco/features/{feature}/resources/{lang}/SKILL.md
|
|
208
216
|
"""
|
|
209
217
|
from monoco.core.feature import MonocoFeature
|
|
210
|
-
|
|
218
|
+
|
|
211
219
|
for feature in self.features:
|
|
212
220
|
if not isinstance(feature, MonocoFeature):
|
|
213
221
|
continue
|
|
214
|
-
|
|
222
|
+
|
|
215
223
|
# Determine feature module path
|
|
216
224
|
# feature.__class__.__module__ is like 'monoco.features.issue.adapter'
|
|
217
|
-
module_parts = feature.__class__.__module__.split(
|
|
218
|
-
if
|
|
225
|
+
module_parts = feature.__class__.__module__.split(".")
|
|
226
|
+
if (
|
|
227
|
+
len(module_parts) >= 3
|
|
228
|
+
and module_parts[0] == "monoco"
|
|
229
|
+
and module_parts[1] == "features"
|
|
230
|
+
):
|
|
219
231
|
feature_name = module_parts[2]
|
|
220
|
-
|
|
232
|
+
|
|
221
233
|
# Construct path to feature resources
|
|
222
234
|
# monoco/features/{feature}/resources/
|
|
223
235
|
feature_dir = self.root / "monoco" / "features" / feature_name
|
|
224
236
|
resources_dir = feature_dir / "resources"
|
|
225
|
-
|
|
237
|
+
|
|
226
238
|
if not resources_dir.exists():
|
|
227
239
|
continue
|
|
228
|
-
|
|
240
|
+
|
|
229
241
|
# Check for SKILL.md in language directories
|
|
230
242
|
for lang_dir in resources_dir.iterdir():
|
|
231
243
|
if lang_dir.is_dir() and (lang_dir / "SKILL.md").exists():
|
|
232
244
|
# Create a Skill instance
|
|
233
245
|
# We need to adapt the Skill class to work with feature resources
|
|
234
|
-
skill = self._create_skill_from_feature(
|
|
246
|
+
skill = self._create_skill_from_feature(
|
|
247
|
+
feature_name, resources_dir
|
|
248
|
+
)
|
|
235
249
|
if skill and skill.is_valid():
|
|
236
250
|
# Use feature name as skill identifier
|
|
237
251
|
skill_key = f"{feature_name}"
|
|
238
252
|
if skill_key not in self.skills:
|
|
239
253
|
self.skills[skill_key] = skill
|
|
240
254
|
break # Only need to detect once per feature
|
|
241
|
-
|
|
242
|
-
def _create_skill_from_feature(
|
|
255
|
+
|
|
256
|
+
def _create_skill_from_feature(
|
|
257
|
+
self, feature_name: str, resources_dir: Path
|
|
258
|
+
) -> Optional[Skill]:
|
|
243
259
|
"""
|
|
244
260
|
Create a Skill instance from a feature's resources directory.
|
|
245
|
-
|
|
261
|
+
|
|
246
262
|
Args:
|
|
247
263
|
feature_name: Name of the feature (e.g., 'issue', 'spike')
|
|
248
264
|
resources_dir: Path to the feature's resources directory
|
|
249
|
-
|
|
265
|
+
|
|
250
266
|
Returns:
|
|
251
267
|
Skill instance or None if creation fails
|
|
252
268
|
"""
|
|
253
269
|
# Use the resources directory as the skill directory
|
|
254
270
|
# The Skill class expects a directory with SKILL.md or {lang}/SKILL.md
|
|
255
271
|
skill = Skill(self.root, resources_dir)
|
|
256
|
-
|
|
272
|
+
|
|
257
273
|
# Use the skill's metadata name if available (e.g., 'monoco-issue')
|
|
258
274
|
# Convert to snake_case for directory name (e.g., 'monoco_issue')
|
|
259
275
|
if skill.metadata and skill.metadata.name:
|
|
260
276
|
# Convert kebab-case to snake_case for directory name
|
|
261
|
-
skill.name = skill.metadata.name.replace(
|
|
277
|
+
skill.name = skill.metadata.name.replace("-", "_")
|
|
262
278
|
else:
|
|
263
279
|
# Fallback to feature name
|
|
264
280
|
skill.name = f"monoco_{feature_name}"
|
|
265
|
-
|
|
281
|
+
|
|
266
282
|
return skill
|
|
267
|
-
|
|
283
|
+
|
|
268
284
|
def list_skills(self) -> List[Skill]:
|
|
269
285
|
"""
|
|
270
286
|
Get all available skills.
|
|
271
|
-
|
|
287
|
+
|
|
272
288
|
Returns:
|
|
273
289
|
List of Skill instances
|
|
274
290
|
"""
|
|
275
291
|
return list(self.skills.values())
|
|
276
|
-
|
|
292
|
+
|
|
277
293
|
def get_skill(self, name: str) -> Optional[Skill]:
|
|
278
294
|
"""
|
|
279
295
|
Get a specific skill by name.
|
|
280
|
-
|
|
296
|
+
|
|
281
297
|
Args:
|
|
282
298
|
name: Skill name
|
|
283
|
-
|
|
299
|
+
|
|
284
300
|
Returns:
|
|
285
301
|
Skill instance or None if not found
|
|
286
302
|
"""
|
|
287
303
|
return self.skills.get(name)
|
|
288
|
-
|
|
304
|
+
|
|
289
305
|
def distribute(
|
|
290
|
-
self,
|
|
291
|
-
target_dir: Path,
|
|
292
|
-
lang: str,
|
|
293
|
-
force: bool = False
|
|
306
|
+
self, target_dir: Path, lang: str, force: bool = False
|
|
294
307
|
) -> Dict[str, bool]:
|
|
295
308
|
"""
|
|
296
309
|
Distribute skills to a target directory.
|
|
297
|
-
|
|
310
|
+
|
|
298
311
|
Args:
|
|
299
312
|
target_dir: Target directory for skill distribution (e.g., .cursor/skills/)
|
|
300
313
|
lang: Language code to distribute (e.g., 'en', 'zh')
|
|
301
314
|
force: Force overwrite even if checksum matches
|
|
302
|
-
|
|
315
|
+
|
|
303
316
|
Returns:
|
|
304
317
|
Dictionary mapping skill names to success status
|
|
305
318
|
"""
|
|
306
319
|
results = {}
|
|
307
|
-
|
|
320
|
+
|
|
308
321
|
# Ensure target directory exists
|
|
309
322
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
310
|
-
|
|
323
|
+
|
|
311
324
|
for skill_name, skill in self.skills.items():
|
|
312
325
|
try:
|
|
313
326
|
# Check if this skill has the requested language
|
|
314
327
|
available_languages = skill.get_languages()
|
|
315
|
-
|
|
328
|
+
|
|
316
329
|
if lang not in available_languages:
|
|
317
|
-
console.print(
|
|
330
|
+
console.print(
|
|
331
|
+
f"[yellow]Skill {skill_name} does not have {lang} version, skipping[/yellow]"
|
|
332
|
+
)
|
|
318
333
|
results[skill_name] = False
|
|
319
334
|
continue
|
|
320
|
-
|
|
335
|
+
|
|
321
336
|
# Distribute the specific language version
|
|
322
337
|
self._distribute_skill_language(skill, target_dir, lang, force)
|
|
323
338
|
results[skill_name] = True
|
|
324
|
-
|
|
339
|
+
|
|
325
340
|
except Exception as e:
|
|
326
|
-
console.print(
|
|
341
|
+
console.print(
|
|
342
|
+
f"[red]Failed to distribute skill {skill_name}: {e}[/red]"
|
|
343
|
+
)
|
|
327
344
|
results[skill_name] = False
|
|
328
|
-
|
|
345
|
+
|
|
329
346
|
return results
|
|
330
|
-
|
|
347
|
+
|
|
331
348
|
def _distribute_skill_language(
|
|
332
|
-
self,
|
|
333
|
-
skill: Skill,
|
|
334
|
-
target_dir: Path,
|
|
335
|
-
lang: str,
|
|
336
|
-
force: bool
|
|
349
|
+
self, skill: Skill, target_dir: Path, lang: str, force: bool
|
|
337
350
|
) -> None:
|
|
338
351
|
"""
|
|
339
352
|
Distribute a specific language version of a skill.
|
|
340
|
-
|
|
353
|
+
|
|
341
354
|
Args:
|
|
342
355
|
skill: Skill instance
|
|
343
356
|
target_dir: Target directory (e.g., .cursor/skills/)
|
|
@@ -346,99 +359,100 @@ class SkillManager:
|
|
|
346
359
|
"""
|
|
347
360
|
# Determine source file (try language subdirectory first)
|
|
348
361
|
source_file = skill.skill_dir / lang / "SKILL.md"
|
|
349
|
-
|
|
362
|
+
|
|
350
363
|
# Fallback to root SKILL.md (legacy pattern)
|
|
351
364
|
if not source_file.exists():
|
|
352
365
|
source_file = skill.skill_file
|
|
353
|
-
|
|
366
|
+
|
|
354
367
|
if not source_file.exists():
|
|
355
|
-
console.print(
|
|
368
|
+
console.print(
|
|
369
|
+
f"[yellow]Source file not found for {skill.name}/{lang}[/yellow]"
|
|
370
|
+
)
|
|
356
371
|
return
|
|
357
|
-
|
|
372
|
+
|
|
358
373
|
# Target path: {target_dir}/{skill_name}/SKILL.md (no language subdirectory)
|
|
359
374
|
target_skill_dir = target_dir / skill.name
|
|
360
|
-
|
|
375
|
+
|
|
361
376
|
# Create target directory
|
|
362
377
|
target_skill_dir.mkdir(parents=True, exist_ok=True)
|
|
363
378
|
target_file = target_skill_dir / "SKILL.md"
|
|
364
|
-
|
|
379
|
+
|
|
365
380
|
# Check if update is needed
|
|
366
381
|
if target_file.exists() and not force:
|
|
367
382
|
source_checksum = skill.get_checksum(lang)
|
|
368
383
|
target_content = target_file.read_bytes()
|
|
369
384
|
target_checksum = hashlib.sha256(target_content).hexdigest()
|
|
370
|
-
|
|
385
|
+
|
|
371
386
|
if source_checksum == target_checksum:
|
|
372
387
|
console.print(f"[dim] = {skill.name}/SKILL.md is up to date[/dim]")
|
|
373
388
|
return
|
|
374
|
-
|
|
389
|
+
|
|
375
390
|
# Copy the file
|
|
376
391
|
shutil.copy2(source_file, target_file)
|
|
377
392
|
console.print(f"[green] ✓ Distributed {skill.name}/SKILL.md ({lang})[/green]")
|
|
378
|
-
|
|
393
|
+
|
|
379
394
|
# Copy additional resources if they exist
|
|
380
395
|
self._copy_skill_resources(skill.skill_dir, target_skill_dir, lang)
|
|
381
|
-
|
|
396
|
+
|
|
382
397
|
def _copy_skill_resources(
|
|
383
|
-
self,
|
|
384
|
-
source_dir: Path,
|
|
385
|
-
target_dir: Path,
|
|
386
|
-
lang: str
|
|
398
|
+
self, source_dir: Path, target_dir: Path, lang: str
|
|
387
399
|
) -> None:
|
|
388
400
|
"""
|
|
389
401
|
Copy additional skill resources (scripts, examples, etc.).
|
|
390
|
-
|
|
402
|
+
|
|
391
403
|
Args:
|
|
392
404
|
source_dir: Source skill directory
|
|
393
405
|
target_dir: Target skill directory
|
|
394
406
|
lang: Language code
|
|
395
407
|
"""
|
|
396
408
|
# Define resource directories to copy
|
|
397
|
-
resource_dirs = [
|
|
398
|
-
|
|
409
|
+
resource_dirs = ["scripts", "examples", "resources"]
|
|
410
|
+
|
|
399
411
|
# Try language subdirectory first (Feature resources pattern)
|
|
400
412
|
source_base = source_dir / lang
|
|
401
|
-
|
|
413
|
+
|
|
402
414
|
# Fallback to root directory (legacy pattern)
|
|
403
415
|
if not source_base.exists():
|
|
404
416
|
source_base = source_dir
|
|
405
|
-
|
|
417
|
+
|
|
406
418
|
for resource_name in resource_dirs:
|
|
407
419
|
source_resource = source_base / resource_name
|
|
408
420
|
if source_resource.exists() and source_resource.is_dir():
|
|
409
421
|
target_resource = target_dir / resource_name
|
|
410
|
-
|
|
422
|
+
|
|
411
423
|
# Remove existing and copy fresh
|
|
412
424
|
if target_resource.exists():
|
|
413
425
|
shutil.rmtree(target_resource)
|
|
414
|
-
|
|
426
|
+
|
|
415
427
|
shutil.copytree(source_resource, target_resource)
|
|
416
|
-
console.print(
|
|
417
|
-
|
|
428
|
+
console.print(
|
|
429
|
+
f"[dim] Copied {resource_name}/ for {source_dir.name}/{lang}[/dim]"
|
|
430
|
+
)
|
|
431
|
+
|
|
418
432
|
def cleanup(self, target_dir: Path) -> None:
|
|
419
433
|
"""
|
|
420
434
|
Remove distributed skills from a target directory.
|
|
421
|
-
|
|
435
|
+
|
|
422
436
|
Args:
|
|
423
437
|
target_dir: Target directory to clean
|
|
424
438
|
"""
|
|
425
439
|
if not target_dir.exists():
|
|
426
440
|
console.print(f"[dim]Target directory does not exist: {target_dir}[/dim]")
|
|
427
441
|
return
|
|
428
|
-
|
|
442
|
+
|
|
429
443
|
removed_count = 0
|
|
430
|
-
|
|
444
|
+
|
|
431
445
|
for skill_name in self.skills.keys():
|
|
432
446
|
skill_target = target_dir / skill_name
|
|
433
447
|
if skill_target.exists():
|
|
434
448
|
shutil.rmtree(skill_target)
|
|
435
449
|
console.print(f"[green] ✓ Removed {skill_name}[/green]")
|
|
436
450
|
removed_count += 1
|
|
437
|
-
|
|
451
|
+
|
|
438
452
|
# Remove empty parent directory if no skills remain
|
|
439
453
|
if target_dir.exists() and not any(target_dir.iterdir()):
|
|
440
454
|
target_dir.rmdir()
|
|
441
455
|
console.print(f"[dim] Removed empty directory: {target_dir}[/dim]")
|
|
442
|
-
|
|
456
|
+
|
|
443
457
|
if removed_count == 0:
|
|
444
458
|
console.print(f"[dim]No skills to remove from {target_dir}[/dim]")
|
monoco/core/state.py
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
from typing import Optional
|
|
2
|
+
from typing import Optional
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
-
from pydantic import BaseModel
|
|
5
|
+
from pydantic import BaseModel
|
|
6
6
|
|
|
7
7
|
logger = logging.getLogger("monoco.core.state")
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
class WorkspaceState(BaseModel):
|
|
10
11
|
"""
|
|
11
12
|
Persisted state for a Monoco workspace (collection of projects).
|
|
12
13
|
Stored in <workspace_root>/.monoco/state.json
|
|
13
14
|
"""
|
|
15
|
+
|
|
14
16
|
last_active_project_id: Optional[str] = None
|
|
15
|
-
|
|
17
|
+
|
|
16
18
|
@classmethod
|
|
17
19
|
def load(cls, workspace_root: Path) -> "WorkspaceState":
|
|
18
20
|
state_file = workspace_root / ".monoco" / "state.json"
|
|
19
21
|
if not state_file.exists():
|
|
20
22
|
return cls()
|
|
21
|
-
|
|
23
|
+
|
|
22
24
|
try:
|
|
23
|
-
content = state_file.read_text(encoding=
|
|
25
|
+
content = state_file.read_text(encoding="utf-8")
|
|
24
26
|
if not content.strip():
|
|
25
27
|
return cls()
|
|
26
28
|
data = json.loads(content)
|
|
@@ -32,22 +34,22 @@ class WorkspaceState(BaseModel):
|
|
|
32
34
|
def save(self, workspace_root: Path):
|
|
33
35
|
state_file = workspace_root / ".monoco" / "state.json"
|
|
34
36
|
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
-
|
|
37
|
+
|
|
36
38
|
try:
|
|
37
39
|
# We merge with existing on disk if possible to preserve unknown keys
|
|
38
40
|
current_data = {}
|
|
39
41
|
if state_file.exists():
|
|
40
42
|
try:
|
|
41
|
-
content = state_file.read_text(encoding=
|
|
43
|
+
content = state_file.read_text(encoding="utf-8")
|
|
42
44
|
if content.strip():
|
|
43
45
|
current_data = json.loads(content)
|
|
44
46
|
except:
|
|
45
47
|
pass
|
|
46
|
-
|
|
48
|
+
|
|
47
49
|
new_data = self.model_dump(exclude_unset=True)
|
|
48
50
|
current_data.update(new_data)
|
|
49
|
-
|
|
50
|
-
state_file.write_text(json.dumps(current_data, indent=2), encoding=
|
|
51
|
+
|
|
52
|
+
state_file.write_text(json.dumps(current_data, indent=2), encoding="utf-8")
|
|
51
53
|
except Exception as e:
|
|
52
54
|
logger.error(f"Failed to save workspace state to {state_file}: {e}")
|
|
53
55
|
raise
|