monoco-toolkit 0.2.8__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.
Files changed (63) hide show
  1. monoco/cli/project.py +35 -31
  2. monoco/cli/workspace.py +26 -16
  3. monoco/core/agent/__init__.py +0 -2
  4. monoco/core/agent/action.py +44 -20
  5. monoco/core/agent/adapters.py +20 -16
  6. monoco/core/agent/protocol.py +5 -4
  7. monoco/core/agent/state.py +21 -21
  8. monoco/core/config.py +90 -33
  9. monoco/core/execution.py +21 -16
  10. monoco/core/feature.py +8 -5
  11. monoco/core/git.py +61 -30
  12. monoco/core/hooks.py +57 -0
  13. monoco/core/injection.py +47 -44
  14. monoco/core/integrations.py +50 -35
  15. monoco/core/lsp.py +12 -1
  16. monoco/core/output.py +35 -16
  17. monoco/core/registry.py +3 -2
  18. monoco/core/setup.py +190 -124
  19. monoco/core/skills.py +121 -107
  20. monoco/core/state.py +12 -10
  21. monoco/core/sync.py +85 -56
  22. monoco/core/telemetry.py +10 -6
  23. monoco/core/workspace.py +26 -19
  24. monoco/daemon/app.py +123 -79
  25. monoco/daemon/commands.py +14 -13
  26. monoco/daemon/models.py +11 -3
  27. monoco/daemon/reproduce_stats.py +8 -8
  28. monoco/daemon/services.py +32 -33
  29. monoco/daemon/stats.py +59 -40
  30. monoco/features/config/commands.py +38 -25
  31. monoco/features/i18n/adapter.py +4 -5
  32. monoco/features/i18n/commands.py +83 -49
  33. monoco/features/i18n/core.py +94 -54
  34. monoco/features/issue/adapter.py +6 -7
  35. monoco/features/issue/commands.py +468 -272
  36. monoco/features/issue/core.py +419 -312
  37. monoco/features/issue/domain/lifecycle.py +33 -23
  38. monoco/features/issue/domain/models.py +71 -38
  39. monoco/features/issue/domain/parser.py +92 -69
  40. monoco/features/issue/domain/workspace.py +19 -16
  41. monoco/features/issue/engine/__init__.py +3 -3
  42. monoco/features/issue/engine/config.py +18 -25
  43. monoco/features/issue/engine/machine.py +72 -39
  44. monoco/features/issue/engine/models.py +4 -2
  45. monoco/features/issue/linter.py +287 -157
  46. monoco/features/issue/lsp/definition.py +26 -19
  47. monoco/features/issue/migration.py +45 -34
  48. monoco/features/issue/models.py +29 -13
  49. monoco/features/issue/monitor.py +24 -8
  50. monoco/features/issue/resources/en/SKILL.md +6 -2
  51. monoco/features/issue/validator.py +383 -208
  52. monoco/features/skills/__init__.py +0 -1
  53. monoco/features/skills/core.py +24 -18
  54. monoco/features/spike/adapter.py +4 -5
  55. monoco/features/spike/commands.py +51 -38
  56. monoco/features/spike/core.py +24 -16
  57. monoco/main.py +34 -21
  58. {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/METADATA +1 -1
  59. monoco_toolkit-0.3.0.dist-info/RECORD +84 -0
  60. monoco_toolkit-0.2.8.dist-info/RECORD +0 -83
  61. {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/WHEEL +0 -0
  62. {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/entry_points.txt +0 -0
  63. {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/licenses/LICENSE +0 -0
monoco/core/sync.py CHANGED
@@ -10,10 +10,11 @@ from rich.console import Console
10
10
 
11
11
  console = Console()
12
12
 
13
+
13
14
  def _get_targets(root: Path, config, cli_target: Optional[Path]) -> List[Path]:
14
15
  """Helper to determine target files."""
15
16
  targets = []
16
-
17
+
17
18
  # 1. CLI Target
18
19
  if cli_target:
19
20
  targets.append(cli_target)
@@ -27,11 +28,9 @@ def _get_targets(root: Path, config, cli_target: Optional[Path]) -> List[Path]:
27
28
 
28
29
  # 3. Registry Defaults (Dynamic Detection)
29
30
  integrations = get_active_integrations(
30
- root,
31
- config_overrides=config.agent.integrations,
32
- auto_detect=True
31
+ root, config_overrides=config.agent.integrations, auto_detect=True
33
32
  )
34
-
33
+
35
34
  if integrations:
36
35
  for integration in integrations.values():
37
36
  targets.append(root / integration.system_prompt_file)
@@ -40,34 +39,40 @@ def _get_targets(root: Path, config, cli_target: Optional[Path]) -> List[Path]:
40
39
  # but we usually want at least one target for a generic sync.
41
40
  defaults = ["GEMINI.md", "CLAUDE.md"]
42
41
  targets.extend([root / fname for fname in defaults])
43
-
44
- return list(set(targets)) # Unique paths
42
+
43
+ return list(set(targets)) # Unique paths
44
+
45
45
 
46
46
  def sync_command(
47
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")
48
+ target: Optional[Path] = typer.Option(
49
+ None,
50
+ "--target",
51
+ "-t",
52
+ help="Specific file to update (default: auto-detect from config or standard files)",
53
+ ),
54
+ check: bool = typer.Option(False, "--check", help="Dry run check mode"),
50
55
  ):
51
56
  """
52
57
  Synchronize Agent Environment (System Prompts & Skills).
53
58
  Aggregates prompts from all active features and injects them into the agent configuration files.
54
59
  """
55
- root = Path.cwd() # TODO: Use workspace root detection properly if needed
56
-
60
+ root = Path.cwd() # TODO: Use workspace root detection properly if needed
61
+
57
62
  # 0. Load Config
58
63
  config = get_config(str(root))
59
-
64
+
60
65
  # 1. Register Features
61
66
  registry = FeatureRegistry()
62
67
  registry.load_defaults()
63
-
68
+
64
69
  # 2. Collect Data
65
70
  collected_prompts = {}
66
-
71
+
67
72
  # Filter features based on config if specified
68
73
  all_features = registry.get_features()
69
74
  active_features = []
70
-
75
+
71
76
  if config.agent.includes:
72
77
  for f in all_features:
73
78
  if f.name in config.agent.includes:
@@ -84,52 +89,64 @@ def sync_command(
84
89
  if data.system_prompts:
85
90
  collected_prompts.update(data.system_prompts)
86
91
  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]")
92
+ console.print(
93
+ f"[red]Error integrating feature {feature.name}: {e}[/red]"
94
+ )
95
+
96
+ console.print(
97
+ f"[blue]Collected {len(collected_prompts)} prompts from {len(active_features)} features.[/blue]"
98
+ )
90
99
 
91
100
  # 3. Distribute Skills
92
- console.print(f"[bold blue]Distributing skills to agent frameworks...[/bold blue]")
93
-
101
+ console.print("[bold blue]Distributing skills to agent frameworks...[/bold blue]")
102
+
94
103
  # Determine language from config
95
- skill_lang = config.i18n.source_lang if config.i18n.source_lang else 'en'
104
+ skill_lang = config.i18n.source_lang if config.i18n.source_lang else "en"
96
105
  console.print(f"[dim] Using language: {skill_lang}[/dim]")
97
-
106
+
98
107
  # Initialize SkillManager with active features
99
108
  skill_manager = SkillManager(root, active_features)
100
-
109
+
101
110
  # Get active integrations
102
111
  integrations = get_active_integrations(
103
- root,
104
- config_overrides=config.agent.integrations,
105
- auto_detect=True
112
+ root, config_overrides=config.agent.integrations, auto_detect=True
106
113
  )
107
-
114
+
108
115
  if integrations:
109
116
  for framework_key, integration in integrations.items():
110
117
  skill_target_dir = root / integration.skill_root_dir
111
- console.print(f"[dim] Distributing to {integration.name} ({skill_target_dir})...[/dim]")
112
-
118
+ console.print(
119
+ f"[dim] Distributing to {integration.name} ({skill_target_dir})...[/dim]"
120
+ )
121
+
113
122
  try:
114
123
  # Distribute only the configured language version
115
- results = skill_manager.distribute(skill_target_dir, lang=skill_lang, force=False)
124
+ results = skill_manager.distribute(
125
+ skill_target_dir, lang=skill_lang, force=False
126
+ )
116
127
  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]")
128
+ console.print(
129
+ f"[green] ✓ Distributed {success_count}/{len(results)} skills to {integration.name}[/green]"
130
+ )
118
131
  except Exception as e:
119
- console.print(f"[red] Failed to distribute skills to {integration.name}: {e}[/red]")
132
+ console.print(
133
+ f"[red] Failed to distribute skills to {integration.name}: {e}[/red]"
134
+ )
120
135
  else:
121
- console.print(f"[yellow]No agent frameworks detected. Skipping skill distribution.[/yellow]")
136
+ console.print(
137
+ "[yellow]No agent frameworks detected. Skipping skill distribution.[/yellow]"
138
+ )
122
139
 
123
140
  # 4. Determine Targets
124
141
  targets = _get_targets(root, config, target)
125
-
142
+
126
143
  # Ensure targets exist for sync
127
144
  final_targets = []
128
145
  for t in targets:
129
146
  if not t.exists():
130
147
  # If explicit target, fail? Or create?
131
148
  # If default, create.
132
- if target:
149
+ if target:
133
150
  # CLI target
134
151
  console.print(f"[yellow]Creating {t.name}...[/yellow]")
135
152
  try:
@@ -152,9 +169,9 @@ def sync_command(
152
169
  # 5. Inject System Prompts
153
170
  for t in final_targets:
154
171
  injector = PromptInjector(t)
155
-
172
+
156
173
  if check:
157
- console.print(f"[dim][Dry Run] Would check/update {t.name}[/dim]")
174
+ console.print(f"[dim][Dry Run] Would check/update {t.name}[/dim]")
158
175
  else:
159
176
  try:
160
177
  changed = injector.inject(collected_prompts)
@@ -165,60 +182,72 @@ def sync_command(
165
182
  except Exception as e:
166
183
  console.print(f"[red]Failed to update {t.name}: {e}[/red]")
167
184
 
185
+
168
186
  def uninstall_command(
169
187
  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)")
188
+ target: Optional[Path] = typer.Option(
189
+ None,
190
+ "--target",
191
+ "-t",
192
+ help="Specific file to clean (default: auto-detect from config or standard files)",
193
+ ),
171
194
  ):
172
195
  """
173
196
  Remove Monoco Managed Block from Agent Environment files and clean up distributed skills.
174
197
  """
175
198
  root = Path.cwd()
176
199
  config = get_config(str(root))
177
-
200
+
178
201
  # 1. Clean up System Prompts
179
202
  targets = _get_targets(root, config, target)
180
-
203
+
181
204
  for t in targets:
182
205
  if not t.exists():
183
206
  if target:
184
- console.print(f"[yellow]Target {t} does not exist.[/yellow]")
207
+ console.print(f"[yellow]Target {t} does not exist.[/yellow]")
185
208
  continue
186
-
209
+
187
210
  injector = PromptInjector(t)
188
211
  try:
189
212
  changed = injector.remove()
190
213
  if changed:
191
- console.print(f"[green]✓ Removed Monoco Managed Block from {t.name}[/green]")
214
+ console.print(
215
+ f"[green]✓ Removed Monoco Managed Block from {t.name}[/green]"
216
+ )
192
217
  else:
193
218
  console.print(f"[dim]= No Monoco Block found in {t.name}[/dim]")
194
219
  except Exception as e:
195
220
  console.print(f"[red]Failed to uninstall from {t.name}: {e}[/red]")
196
-
221
+
197
222
  # 2. Clean up Skills
198
- console.print(f"[bold blue]Cleaning up distributed skills...[/bold blue]")
199
-
223
+ console.print("[bold blue]Cleaning up distributed skills...[/bold blue]")
224
+
200
225
  # Load features to get skill list
201
226
  registry = FeatureRegistry()
202
227
  registry.load_defaults()
203
228
  active_features = registry.get_features()
204
-
229
+
205
230
  skill_manager = SkillManager(root, active_features)
206
-
231
+
207
232
  # Get active integrations
208
233
  integrations = get_active_integrations(
209
- root,
210
- config_overrides=config.agent.integrations,
211
- auto_detect=True
234
+ root, config_overrides=config.agent.integrations, auto_detect=True
212
235
  )
213
-
236
+
214
237
  if integrations:
215
238
  for framework_key, integration in integrations.items():
216
239
  skill_target_dir = root / integration.skill_root_dir
217
- console.print(f"[dim] Cleaning {integration.name} ({skill_target_dir})...[/dim]")
218
-
240
+ console.print(
241
+ f"[dim] Cleaning {integration.name} ({skill_target_dir})...[/dim]"
242
+ )
243
+
219
244
  try:
220
245
  skill_manager.cleanup(skill_target_dir)
221
246
  except Exception as e:
222
- console.print(f"[red] Failed to clean skills from {integration.name}: {e}[/red]")
247
+ console.print(
248
+ f"[red] Failed to clean skills from {integration.name}: {e}[/red]"
249
+ )
223
250
  else:
224
- console.print(f"[yellow]No agent frameworks detected. Skipping skill cleanup.[/yellow]")
251
+ console.print(
252
+ "[yellow]No agent frameworks detected. Skipping skill cleanup.[/yellow]"
253
+ )
monoco/core/telemetry.py CHANGED
@@ -7,14 +7,15 @@ from pathlib import Path
7
7
  from typing import Optional, Dict, Any
8
8
  from monoco.core.config import get_config
9
9
 
10
- POSTHOG_API_KEY = "phc_MndV8H8v0W3P7Yv1P7Z8X7X7X7X7X7X7X7X7"
10
+ POSTHOG_API_KEY = "phc_MndV8H8v0W3P7Yv1P7Z8X7X7X7X7X7X7X7X7"
11
11
  POSTHOG_HOST = "https://app.posthog.com"
12
12
 
13
+
13
14
  class Telemetry:
14
15
  def __init__(self):
15
16
  self.config = get_config()
16
17
  self._device_id = self._get_or_create_device_id()
17
-
18
+
18
19
  def _get_or_create_device_id(self) -> str:
19
20
  state_path = Path.home() / ".monoco" / "state.json"
20
21
  if state_path.exists():
@@ -25,7 +26,7 @@ class Telemetry:
25
26
  return state["device_id"]
26
27
  except Exception:
27
28
  pass
28
-
29
+
29
30
  device_id = str(uuid.uuid4())
30
31
  state_path.parent.mkdir(parents=True, exist_ok=True)
31
32
  try:
@@ -44,7 +45,7 @@ class Telemetry:
44
45
  # Notify user on first use if not configured
45
46
  if self.config.telemetry.enabled is None:
46
47
  # We don't want to spam, but we must be transparent
47
- # This is a one-time notice in a session via a class-level flag?
48
+ # This is a one-time notice in a session via a class-level flag?
48
49
  # Or just rely on the fact that 'init' will fix it.
49
50
  # To be safe and minimal, we'll just skip capture if not explicitly enabled
50
51
  return
@@ -68,20 +69,23 @@ class Telemetry:
68
69
  "api_key": POSTHOG_API_KEY,
69
70
  "event": namespaced_event,
70
71
  "properties": props,
71
- "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z")
72
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
72
73
  }
73
74
 
74
75
  # Send asynchronously? For now, we'll do a simple non-blocking-ish call
75
76
  try:
76
77
  import httpx
78
+
77
79
  httpx.post(f"{POSTHOG_HOST}/capture/", json=data, timeout=1.0)
78
80
  except ImportError:
79
- pass # Telemetry is optional
81
+ pass # Telemetry is optional
80
82
  except Exception:
81
83
  pass
82
84
 
85
+
83
86
  _instance = None
84
87
 
88
+
85
89
  def capture_event(event: str, properties: Optional[Dict[str, Any]] = None):
86
90
  global _instance
87
91
  if _instance is None:
monoco/core/workspace.py CHANGED
@@ -1,14 +1,16 @@
1
1
  import os
2
2
  from pathlib import Path
3
- from typing import List, Optional, Dict
4
- from pydantic import BaseModel, Field, ConfigDict
3
+ from typing import List, Optional
4
+ from pydantic import BaseModel, ConfigDict
5
5
 
6
6
  from monoco.core.config import get_config, MonocoConfig
7
7
 
8
+
8
9
  class MonocoProject(BaseModel):
9
10
  """
10
11
  Representation of a single Monoco project.
11
12
  """
13
+
12
14
  id: str # Unique ID within the workspace (usually the directory name)
13
15
  name: str
14
16
  path: Path
@@ -23,6 +25,7 @@ class MonocoProject(BaseModel):
23
25
 
24
26
  model_config = ConfigDict(arbitrary_types_allowed=True)
25
27
 
28
+
26
29
  def is_project_root(path: Path) -> bool:
27
30
  """
28
31
  Check if a directory serves as a Monoco project root.
@@ -31,66 +34,70 @@ def is_project_root(path: Path) -> bool:
31
34
  """
32
35
  if not path.is_dir():
33
36
  return False
34
-
37
+
35
38
  return (path / ".monoco").is_dir()
36
39
 
40
+
37
41
  def load_project(path: Path) -> Optional[MonocoProject]:
38
42
  """Load a project from a path if it is a valid project root."""
39
43
  if not is_project_root(path):
40
44
  return None
41
-
45
+
42
46
  try:
43
47
  config = get_config(str(path))
44
48
  # If name is default, use directory name
45
49
  name = config.project.name
46
50
  if name == "Monoco Project":
47
51
  name = path.name
48
-
49
- return MonocoProject(
50
- id=path.name,
51
- name=name,
52
- path=path,
53
- config=config
54
- )
52
+
53
+ return MonocoProject(id=path.name, name=name, path=path, config=config)
55
54
  except Exception:
56
55
  return None
57
56
 
57
+
58
58
  def find_projects(workspace_root: Path) -> List[MonocoProject]:
59
59
  """
60
60
  Scan for projects in a workspace.
61
61
  Returns list of MonocoProject instances.
62
62
  """
63
63
  projects = []
64
-
64
+
65
65
  # 1. Check workspace root itself
66
66
  root_project = load_project(workspace_root)
67
67
  if root_project:
68
68
  projects.append(root_project)
69
-
69
+
70
70
  # 2. Recursive Scan
71
71
  for root, dirs, files in os.walk(workspace_root):
72
72
  # Skip hidden directories and node_modules
73
- dirs[:] = [d for d in dirs if not d.startswith('.') and d != 'node_modules' and d != 'venv']
74
-
73
+ dirs[:] = [
74
+ d
75
+ for d in dirs
76
+ if not d.startswith(".") and d != "node_modules" and d != "venv"
77
+ ]
78
+
75
79
  for d in dirs:
76
80
  project_path = Path(root) / d
77
81
  # Avoid re-adding root if it was somehow added (unlikely here)
78
- if project_path == workspace_root: continue
79
-
82
+ if project_path == workspace_root:
83
+ continue
84
+
80
85
  if is_project_root(project_path):
81
86
  p = load_project(project_path)
82
87
  if p:
83
88
  projects.append(p)
84
-
89
+
85
90
  return projects
86
91
 
92
+
87
93
  class Workspace(BaseModel):
88
94
  """
89
95
  Standardized Workspace primitive.
90
96
  """
97
+
91
98
  root: Path
92
99
  projects: List[MonocoProject] = []
93
-
100
+
94
101
  @classmethod
95
102
  def discover(cls, root: Path) -> "Workspace":
96
103
  projects = find_projects(root)