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