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.
- 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 +468 -272
- monoco/features/issue/core.py +419 -312
- 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 +287 -157
- monoco/features/issue/lsp/definition.py +26 -19
- monoco/features/issue/migration.py +45 -34
- monoco/features/issue/models.py +29 -13
- monoco/features/issue/monitor.py +24 -8
- monoco/features/issue/resources/en/SKILL.md +6 -2
- monoco/features/issue/validator.py +383 -208
- 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.8.dist-info → monoco_toolkit-0.3.0.dist-info}/METADATA +1 -1
- monoco_toolkit-0.3.0.dist-info/RECORD +84 -0
- monoco_toolkit-0.2.8.dist-info/RECORD +0 -83
- {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.2.8.dist-info → monoco_toolkit-0.3.0.dist-info}/licenses/LICENSE +0 -0
monoco/core/setup.py
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import subprocess
|
|
3
|
+
import sys
|
|
3
4
|
import yaml
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Optional
|
|
7
|
+
|
|
6
8
|
import typer
|
|
9
|
+
from prompt_toolkit.application import Application
|
|
10
|
+
from prompt_toolkit.layout.containers import Window, HSplit
|
|
11
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
12
|
+
from prompt_toolkit.layout.layout import Layout
|
|
13
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
14
|
+
from prompt_toolkit.styles import Style
|
|
7
15
|
from rich.console import Console
|
|
8
|
-
from monoco.core.output import print_output
|
|
9
16
|
|
|
10
17
|
console = Console()
|
|
11
18
|
|
|
19
|
+
|
|
12
20
|
def get_git_user() -> str:
|
|
13
21
|
try:
|
|
14
22
|
result = subprocess.run(
|
|
15
|
-
["git", "config", "user.name"],
|
|
16
|
-
capture_output=True,
|
|
17
|
-
text=True,
|
|
18
|
-
timeout=1
|
|
23
|
+
["git", "config", "user.name"],
|
|
24
|
+
capture_output=True,
|
|
25
|
+
text=True,
|
|
26
|
+
timeout=1,
|
|
19
27
|
)
|
|
20
28
|
return result.stdout.strip()
|
|
21
29
|
except Exception:
|
|
22
30
|
return ""
|
|
23
31
|
|
|
32
|
+
|
|
24
33
|
def generate_key(name: str) -> str:
|
|
25
34
|
"""Generate a 3-4 letter uppercase key from name."""
|
|
26
35
|
# Strategy 1: Upper case of first letters of words
|
|
@@ -29,128 +38,147 @@ def generate_key(name: str) -> str:
|
|
|
29
38
|
candidate = "".join(p[0] for p in parts[:4]).upper()
|
|
30
39
|
if len(candidate) >= 2:
|
|
31
40
|
return candidate
|
|
32
|
-
|
|
41
|
+
|
|
33
42
|
# Strategy 2: First 3 letters
|
|
34
43
|
return name[:3].upper()
|
|
35
44
|
|
|
36
|
-
from prompt_toolkit.application import Application
|
|
37
|
-
from prompt_toolkit.layout.containers import Window, HSplit
|
|
38
|
-
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
39
|
-
from prompt_toolkit.layout.layout import Layout
|
|
40
|
-
from prompt_toolkit.key_binding import KeyBindings
|
|
41
|
-
from prompt_toolkit.styles import Style
|
|
42
|
-
import sys
|
|
43
45
|
|
|
44
46
|
def ask_with_selection(message: str, default: str) -> str:
|
|
45
47
|
"""Provides a selection-based prompt for stable rendering."""
|
|
46
48
|
options = [f"{default} (Default)", "Custom Input..."]
|
|
47
49
|
selected_index = 0
|
|
48
|
-
|
|
50
|
+
|
|
49
51
|
kb = KeyBindings()
|
|
50
|
-
|
|
51
|
-
@kb.add(
|
|
52
|
-
@kb.add(
|
|
52
|
+
|
|
53
|
+
@kb.add("up")
|
|
54
|
+
@kb.add("k")
|
|
53
55
|
def _(event):
|
|
54
56
|
nonlocal selected_index
|
|
55
57
|
selected_index = (selected_index - 1) % len(options)
|
|
56
58
|
|
|
57
|
-
@kb.add(
|
|
58
|
-
@kb.add(
|
|
59
|
+
@kb.add("down")
|
|
60
|
+
@kb.add("j")
|
|
59
61
|
def _(event):
|
|
60
62
|
nonlocal selected_index
|
|
61
63
|
selected_index = (selected_index + 1) % len(options)
|
|
62
64
|
|
|
63
|
-
@kb.add(
|
|
65
|
+
@kb.add("enter")
|
|
64
66
|
def _(event):
|
|
65
67
|
event.app.exit(result=selected_index)
|
|
66
68
|
|
|
67
|
-
@kb.add(
|
|
69
|
+
@kb.add("c-c")
|
|
68
70
|
def _(event):
|
|
69
71
|
console.print("\n[red]Aborted by user.[/red]")
|
|
70
72
|
sys.exit(0)
|
|
71
73
|
|
|
72
74
|
def get_text():
|
|
73
75
|
# Render the menu with explicit highlighting
|
|
74
|
-
res = [(
|
|
76
|
+
res = [("class:message", f"{message}:\n")]
|
|
75
77
|
for i, opt in enumerate(options):
|
|
76
78
|
if i == selected_index:
|
|
77
|
-
res.append((
|
|
79
|
+
res.append(("class:selected", f" ➔ {opt}\n"))
|
|
78
80
|
else:
|
|
79
|
-
res.append((
|
|
81
|
+
res.append(("class:unselected", f" {opt}\n"))
|
|
80
82
|
return res
|
|
81
83
|
|
|
82
|
-
style = Style.from_dict(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
style = Style.from_dict(
|
|
85
|
+
{
|
|
86
|
+
"message": "bold #ffffff",
|
|
87
|
+
"selected": "bold #00ff00", # High contrast green
|
|
88
|
+
"unselected": "#888888",
|
|
89
|
+
}
|
|
90
|
+
)
|
|
87
91
|
|
|
88
92
|
# Run a mini application to handle the selection
|
|
89
93
|
app = Application(
|
|
90
|
-
layout=Layout(
|
|
94
|
+
layout=Layout(
|
|
95
|
+
HSplit(
|
|
96
|
+
[
|
|
97
|
+
Window(
|
|
98
|
+
content=FormattedTextControl(get_text), height=len(options) + 1
|
|
99
|
+
)
|
|
100
|
+
]
|
|
101
|
+
)
|
|
102
|
+
),
|
|
91
103
|
key_bindings=kb,
|
|
92
104
|
style=style,
|
|
93
105
|
full_screen=False,
|
|
94
106
|
)
|
|
95
|
-
|
|
107
|
+
|
|
96
108
|
# Flush stdout to ensure previous output is visible
|
|
97
109
|
sys.stdout.flush()
|
|
98
|
-
|
|
110
|
+
|
|
99
111
|
choice = app.run()
|
|
100
|
-
|
|
112
|
+
|
|
101
113
|
if choice == 0:
|
|
102
114
|
return default
|
|
103
115
|
else:
|
|
104
116
|
# Prompt for custom input
|
|
105
117
|
from prompt_toolkit import prompt
|
|
118
|
+
|
|
106
119
|
return prompt(f"Enter custom {message.lower()}: ").strip() or default
|
|
107
120
|
|
|
121
|
+
|
|
108
122
|
def init_cli(
|
|
109
|
-
ctx: typer.Context,
|
|
110
|
-
global_only: bool = typer.Option(
|
|
111
|
-
|
|
123
|
+
ctx: typer.Context,
|
|
124
|
+
global_only: bool = typer.Option(
|
|
125
|
+
False, "--global", help="Only configure global user settings"
|
|
126
|
+
),
|
|
127
|
+
project_only: bool = typer.Option(
|
|
128
|
+
False, "--project", help="Only configure current project"
|
|
129
|
+
),
|
|
112
130
|
# Non-interactive arguments
|
|
113
131
|
name: Optional[str] = typer.Option(None, "--name", "-n", help="Project Name"),
|
|
114
132
|
key: Optional[str] = typer.Option(None, "--key", "-k", help="Project Key"),
|
|
115
133
|
author: Optional[str] = typer.Option(None, "--author", "-a", help="Author Name"),
|
|
116
|
-
telemetry: Optional[bool] = typer.Option(
|
|
134
|
+
telemetry: Optional[bool] = typer.Option(
|
|
135
|
+
None, "--telemetry/--no-telemetry", help="Enable/Disable telemetry"
|
|
136
|
+
),
|
|
117
137
|
):
|
|
118
138
|
"""
|
|
119
139
|
Initialize Monoco configuration (Global and/or Project).
|
|
120
140
|
"""
|
|
121
141
|
# Force non-interactive for now as requested
|
|
122
142
|
interactive = False
|
|
123
|
-
|
|
143
|
+
|
|
124
144
|
home_dir = Path.home() / ".monoco"
|
|
125
145
|
global_config_path = home_dir / "config.yaml"
|
|
126
|
-
|
|
146
|
+
|
|
127
147
|
# --- 1. Global Configuration ---
|
|
128
148
|
if not project_only:
|
|
129
149
|
if not global_config_path.exists() or global_only:
|
|
130
150
|
console.rule("[bold blue]Global Setup[/bold blue]")
|
|
131
|
-
|
|
151
|
+
|
|
132
152
|
# Ensure ~/.monoco exists
|
|
133
153
|
home_dir.mkdir(parents=True, exist_ok=True)
|
|
134
|
-
|
|
154
|
+
|
|
135
155
|
default_author = get_git_user() or os.getenv("USER", "developer")
|
|
136
|
-
|
|
156
|
+
|
|
137
157
|
if author is None:
|
|
138
158
|
if interactive:
|
|
139
|
-
author = ask_with_selection(
|
|
159
|
+
author = ask_with_selection(
|
|
160
|
+
"Your Name (for issue tracking)", default_author
|
|
161
|
+
)
|
|
140
162
|
else:
|
|
141
|
-
# Fallback or Error?
|
|
163
|
+
# Fallback or Error?
|
|
142
164
|
# For global author, we can use default if not provided, or error?
|
|
143
165
|
# User said "Directly error saying what field is missing"
|
|
144
166
|
# But author has a reasonable default. Let's try to use default if available, else error.
|
|
145
167
|
if not default_author:
|
|
146
|
-
|
|
147
|
-
|
|
168
|
+
console.print(
|
|
169
|
+
"[red]Error:[/red] Missing required field: --author"
|
|
170
|
+
)
|
|
171
|
+
raise typer.Exit(code=1)
|
|
148
172
|
author = default_author
|
|
149
173
|
|
|
150
174
|
if telemetry is None:
|
|
151
175
|
if interactive:
|
|
152
176
|
from rich.prompt import Confirm
|
|
153
|
-
|
|
177
|
+
|
|
178
|
+
telemetry = Confirm.ask(
|
|
179
|
+
"Enable anonymous telemetry to help improve Monoco?",
|
|
180
|
+
default=True,
|
|
181
|
+
)
|
|
154
182
|
else:
|
|
155
183
|
# Default to True or False? Let's default to False for non-interactive safety or True?
|
|
156
184
|
# Usually explicit is better. Let's assume False if not specified in non-interactive.
|
|
@@ -163,15 +191,15 @@ def init_cli(
|
|
|
163
191
|
"core": {
|
|
164
192
|
"author": author,
|
|
165
193
|
},
|
|
166
|
-
"telemetry": {
|
|
167
|
-
"enabled": telemetry
|
|
168
|
-
}
|
|
194
|
+
"telemetry": {"enabled": telemetry},
|
|
169
195
|
}
|
|
170
|
-
|
|
196
|
+
|
|
171
197
|
with open(global_config_path, "w") as f:
|
|
172
198
|
yaml.dump(user_config, f, default_flow_style=False)
|
|
173
|
-
|
|
174
|
-
console.print(
|
|
199
|
+
|
|
200
|
+
console.print(
|
|
201
|
+
f"[green]✓ Global config saved to {global_config_path}[/green]\n"
|
|
202
|
+
)
|
|
175
203
|
|
|
176
204
|
if global_only:
|
|
177
205
|
return
|
|
@@ -181,91 +209,129 @@ def init_cli(
|
|
|
181
209
|
project_config_dir = cwd / ".monoco"
|
|
182
210
|
workspace_config_path = project_config_dir / "workspace.yaml"
|
|
183
211
|
project_config_path = project_config_dir / "project.yaml"
|
|
184
|
-
|
|
212
|
+
|
|
213
|
+
project_initialized = False
|
|
214
|
+
|
|
185
215
|
# Check if we should init project
|
|
186
216
|
if workspace_config_path.exists() or project_config_path.exists():
|
|
187
217
|
if interactive:
|
|
188
218
|
from rich.prompt import Confirm
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
console.rule("[bold blue]Project Setup[/bold blue]")
|
|
197
|
-
|
|
198
|
-
default_name = cwd.name
|
|
199
|
-
|
|
200
|
-
if name is None:
|
|
201
|
-
if interactive:
|
|
202
|
-
name = ask_with_selection("Project Name", default_name)
|
|
219
|
+
|
|
220
|
+
if not Confirm.ask(
|
|
221
|
+
f"Project/Workspace config already exists in [dim]{project_config_dir}[/dim]. Overwrite?"
|
|
222
|
+
):
|
|
223
|
+
console.print("[dim]Skipping configuration overwrite.[/dim]")
|
|
224
|
+
project_initialized = True
|
|
203
225
|
else:
|
|
204
|
-
console.print(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
226
|
+
console.print(
|
|
227
|
+
f"[dim]Project/Workspace config already exists in {project_config_dir}. skipping generation.[/dim]"
|
|
228
|
+
)
|
|
229
|
+
project_initialized = True
|
|
230
|
+
|
|
231
|
+
# Load existing config for downstream usage
|
|
232
|
+
if workspace_config_path.exists():
|
|
233
|
+
try:
|
|
234
|
+
with open(workspace_config_path, "r") as f:
|
|
235
|
+
workspace_config = yaml.safe_load(f) or {}
|
|
236
|
+
except Exception:
|
|
237
|
+
workspace_config = {}
|
|
214
238
|
else:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
239
|
+
workspace_config = {}
|
|
240
|
+
|
|
241
|
+
if not project_initialized:
|
|
242
|
+
console.rule("[bold blue]Project Setup[/bold blue]")
|
|
243
|
+
|
|
244
|
+
default_name = cwd.name
|
|
245
|
+
|
|
246
|
+
if name is None:
|
|
247
|
+
if interactive:
|
|
248
|
+
name = ask_with_selection("Project Name", default_name)
|
|
249
|
+
else:
|
|
250
|
+
console.print("[red]Error:[/red] Missing required field: --name")
|
|
251
|
+
raise typer.Exit(code=1)
|
|
252
|
+
|
|
253
|
+
project_name = name
|
|
254
|
+
|
|
255
|
+
default_key = generate_key(project_name)
|
|
256
|
+
|
|
257
|
+
if key is None:
|
|
258
|
+
if interactive:
|
|
259
|
+
key = ask_with_selection("Project Key (prefix for issues)", default_key)
|
|
260
|
+
else:
|
|
261
|
+
console.print("[red]Error:[/red] Missing required field: --key")
|
|
262
|
+
raise typer.Exit(code=1)
|
|
263
|
+
|
|
264
|
+
project_key = key
|
|
265
|
+
|
|
266
|
+
project_config_dir.mkdir(exist_ok=True)
|
|
267
|
+
|
|
268
|
+
# 2a. Create project.yaml (Identity)
|
|
269
|
+
project_config = {"project": {"name": project_name, "key": project_key}}
|
|
270
|
+
|
|
271
|
+
with open(project_config_path, "w") as f:
|
|
272
|
+
yaml.dump(project_config, f, default_flow_style=False)
|
|
273
|
+
|
|
274
|
+
# 2b. Create workspace.yaml (Environment)
|
|
275
|
+
workspace_config = {
|
|
276
|
+
"paths": {"issues": "Issues", "spikes": ".references"},
|
|
277
|
+
"hooks": {"pre-commit": "monoco issue lint --recursive"},
|
|
240
278
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
279
|
+
|
|
280
|
+
with open(workspace_config_path, "w") as f:
|
|
281
|
+
yaml.dump(workspace_config, f, default_flow_style=False)
|
|
282
|
+
|
|
283
|
+
# 2c. Generate Config Template (Optional - might need update)
|
|
284
|
+
# For now, let's skip template generation or update it later.
|
|
285
|
+
# Or generate a workspace_template.yaml
|
|
286
|
+
|
|
287
|
+
console.print(f"[green]✓ Project initialized in {cwd}[/green]")
|
|
288
|
+
console.print("[dim] - Identity: .monoco/project.yaml[/dim]")
|
|
289
|
+
console.print("[dim] - Environment: .monoco/workspace.yaml[/dim]")
|
|
290
|
+
|
|
291
|
+
# Common Post-Init Logic (Idempotent)
|
|
292
|
+
|
|
293
|
+
# Retrieve project key if we skipped initialization
|
|
294
|
+
if project_initialized:
|
|
295
|
+
# Try to read project key from file or just fallback
|
|
296
|
+
if project_config_path.exists():
|
|
297
|
+
try:
|
|
298
|
+
with open(project_config_path, "r") as f:
|
|
299
|
+
p_conf = yaml.safe_load(f)
|
|
300
|
+
project_key = p_conf.get("project", {}).get("key", "MON")
|
|
301
|
+
except:
|
|
302
|
+
project_key = "MON"
|
|
303
|
+
else:
|
|
304
|
+
project_key = "MON"
|
|
253
305
|
|
|
254
306
|
# Check for issue feature init (this logic was implicit in caller?)
|
|
255
307
|
# No, init_cli is the main logic.
|
|
256
|
-
|
|
308
|
+
|
|
257
309
|
# Initialize basic directories
|
|
258
310
|
(cwd / "Issues").mkdir(exist_ok=True)
|
|
259
311
|
(cwd / ".references").mkdir(exist_ok=True)
|
|
260
|
-
|
|
261
|
-
# Initialize Agent Resources
|
|
312
|
+
|
|
313
|
+
# Initialize Agent Resources (Deprecated)
|
|
314
|
+
# Removing reference to monoco.features.agent
|
|
315
|
+
# try:
|
|
316
|
+
# from monoco.features.agent.core import init_agent_resources
|
|
317
|
+
# init_agent_resources(cwd)
|
|
318
|
+
# console.print(f"[dim] - Agent Resources: .monoco/actions/*.prompty[/dim]")
|
|
319
|
+
# except Exception as e:
|
|
320
|
+
# console.print(f"[yellow]Warning: Failed to init agent resources: {e}[/yellow]")
|
|
321
|
+
|
|
322
|
+
# Initialize Hooks
|
|
262
323
|
try:
|
|
263
|
-
from monoco.
|
|
264
|
-
|
|
265
|
-
|
|
324
|
+
from monoco.core.hooks import install_hooks
|
|
325
|
+
|
|
326
|
+
# Re-load config to get the just-written hooks (or default ones)
|
|
327
|
+
# Actually we have the dict right here in workspace_config['hooks']
|
|
328
|
+
hooks_config = workspace_config.get("hooks", {})
|
|
329
|
+
if hooks_config:
|
|
330
|
+
install_hooks(cwd, hooks_config)
|
|
266
331
|
except Exception as e:
|
|
267
|
-
console.print(f"[yellow]Warning: Failed to
|
|
332
|
+
console.print(f"[yellow]Warning: Failed to install hooks: {e}[/yellow]")
|
|
268
333
|
|
|
269
334
|
console.print("\n[bold green]✓ Monoco Project Initialized![/bold green]")
|
|
270
|
-
console.print(
|
|
271
|
-
|
|
335
|
+
console.print(
|
|
336
|
+
f"Access configured! issues will be created as [bold]{project_key}-XXX[/bold]"
|
|
337
|
+
)
|