monoco-toolkit 0.2.4__py3-none-any.whl → 0.2.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/cli/project.py +15 -7
- monoco/cli/workspace.py +11 -3
- monoco/core/agent/adapters.py +24 -1
- monoco/core/config.py +81 -3
- monoco/core/integrations.py +8 -0
- monoco/core/lsp.py +7 -0
- monoco/core/output.py +8 -1
- monoco/core/resources/en/SKILL.md +1 -1
- monoco/core/setup.py +8 -1
- monoco/daemon/app.py +18 -12
- monoco/features/agent/commands.py +94 -17
- monoco/features/agent/core.py +48 -0
- monoco/features/agent/resources/en/critique.prompty +16 -0
- monoco/features/agent/resources/en/develop.prompty +16 -0
- monoco/features/agent/resources/en/investigate.prompty +16 -0
- monoco/features/agent/resources/en/refine.prompty +14 -0
- monoco/features/agent/resources/en/verify.prompty +16 -0
- monoco/features/agent/resources/zh/critique.prompty +18 -0
- monoco/features/agent/resources/zh/develop.prompty +18 -0
- monoco/features/agent/resources/zh/investigate.prompty +18 -0
- monoco/features/agent/resources/zh/refine.prompty +16 -0
- monoco/features/agent/resources/zh/verify.prompty +18 -0
- monoco/features/config/commands.py +35 -14
- monoco/features/i18n/commands.py +89 -10
- monoco/features/i18n/core.py +112 -16
- monoco/features/issue/commands.py +254 -85
- monoco/features/issue/core.py +142 -119
- monoco/features/issue/domain/__init__.py +0 -0
- monoco/features/issue/domain/lifecycle.py +126 -0
- monoco/features/issue/domain/models.py +170 -0
- monoco/features/issue/domain/parser.py +223 -0
- monoco/features/issue/domain/workspace.py +104 -0
- monoco/features/issue/engine/__init__.py +22 -0
- monoco/features/issue/engine/config.py +189 -0
- monoco/features/issue/engine/machine.py +185 -0
- monoco/features/issue/engine/models.py +18 -0
- monoco/features/issue/linter.py +32 -11
- monoco/features/issue/lsp/__init__.py +3 -0
- monoco/features/issue/lsp/definition.py +72 -0
- monoco/features/issue/models.py +8 -8
- monoco/features/issue/validator.py +204 -65
- monoco/features/spike/commands.py +45 -24
- monoco/features/spike/core.py +5 -22
- monoco/main.py +11 -17
- {monoco_toolkit-0.2.4.dist-info → monoco_toolkit-0.2.6.dist-info}/METADATA +1 -1
- monoco_toolkit-0.2.6.dist-info/RECORD +96 -0
- monoco/features/issue/executions/refine.md +0 -26
- monoco/features/pty/core.py +0 -185
- monoco/features/pty/router.py +0 -138
- monoco/features/pty/server.py +0 -56
- monoco_toolkit-0.2.4.dist-info/RECORD +0 -78
- {monoco_toolkit-0.2.4.dist-info → monoco_toolkit-0.2.6.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.2.4.dist-info → monoco_toolkit-0.2.6.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.2.4.dist-info → monoco_toolkit-0.2.6.dist-info}/licenses/LICENSE +0 -0
monoco/cli/project.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import typer
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import Optional, Annotated
|
|
4
4
|
from rich.console import Console
|
|
5
5
|
from rich.table import Table
|
|
6
6
|
import yaml
|
|
@@ -9,20 +9,21 @@ import os
|
|
|
9
9
|
|
|
10
10
|
from monoco.core.workspace import find_projects, is_project_root
|
|
11
11
|
from monoco.core.config import get_config
|
|
12
|
+
from monoco.core.output import AgentOutput, OutputManager
|
|
12
13
|
|
|
13
14
|
app = typer.Typer(help="Manage Monoco Projects")
|
|
14
15
|
console = Console()
|
|
15
16
|
|
|
16
17
|
@app.command("list")
|
|
17
18
|
def list_projects(
|
|
18
|
-
|
|
19
|
+
json: AgentOutput = False,
|
|
19
20
|
root: Optional[str] = typer.Option(None, "--root", help="Workspace root")
|
|
20
21
|
):
|
|
21
22
|
"""List all discovered projects in the workspace."""
|
|
22
23
|
cwd = Path(root).resolve() if root else Path.cwd()
|
|
23
24
|
projects = find_projects(cwd)
|
|
24
25
|
|
|
25
|
-
if
|
|
26
|
+
if OutputManager.is_agent_mode():
|
|
26
27
|
data = [
|
|
27
28
|
{
|
|
28
29
|
"id": p.id,
|
|
@@ -32,7 +33,7 @@ def list_projects(
|
|
|
32
33
|
}
|
|
33
34
|
for p in projects
|
|
34
35
|
]
|
|
35
|
-
print(
|
|
36
|
+
OutputManager.print(data)
|
|
36
37
|
else:
|
|
37
38
|
table = Table(title=f"Projects in {cwd}")
|
|
38
39
|
table.add_column("ID", style="cyan")
|
|
@@ -53,14 +54,15 @@ def list_projects(
|
|
|
53
54
|
def init_project(
|
|
54
55
|
name: str = typer.Option(..., "--name", "-n", help="Project Name"),
|
|
55
56
|
key: str = typer.Option(..., "--key", "-k", help="Project Key"),
|
|
56
|
-
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config")
|
|
57
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config"),
|
|
58
|
+
json: AgentOutput = False,
|
|
57
59
|
):
|
|
58
60
|
"""Initialize a new project in the current directory."""
|
|
59
61
|
cwd = Path.cwd()
|
|
60
62
|
project_config_path = cwd / ".monoco" / "project.yaml"
|
|
61
63
|
|
|
62
64
|
if project_config_path.exists() and not force:
|
|
63
|
-
|
|
65
|
+
OutputManager.error(f"Project already initialized in {cwd}. Use --force to overwrite.")
|
|
64
66
|
raise typer.Exit(code=1)
|
|
65
67
|
|
|
66
68
|
cwd.mkdir(parents=True, exist_ok=True)
|
|
@@ -76,4 +78,10 @@ def init_project(
|
|
|
76
78
|
with open(project_config_path, "w") as f:
|
|
77
79
|
yaml.dump(config, f, default_flow_style=False)
|
|
78
80
|
|
|
79
|
-
|
|
81
|
+
OutputManager.print({
|
|
82
|
+
"status": "initialized",
|
|
83
|
+
"name": name,
|
|
84
|
+
"key": key,
|
|
85
|
+
"path": str(cwd),
|
|
86
|
+
"config_file": str(project_config_path)
|
|
87
|
+
})
|
monoco/cli/workspace.py
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import typer
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from rich.console import Console
|
|
4
|
+
from typing import Annotated
|
|
4
5
|
import yaml
|
|
5
6
|
import json
|
|
6
7
|
|
|
8
|
+
from monoco.core.output import AgentOutput, OutputManager
|
|
9
|
+
|
|
7
10
|
app = typer.Typer(help="Manage Monoco Workspace")
|
|
8
11
|
console = Console()
|
|
9
12
|
|
|
10
13
|
@app.command("init")
|
|
11
14
|
def init_workspace(
|
|
12
|
-
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config")
|
|
15
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config"),
|
|
16
|
+
json: AgentOutput = False,
|
|
13
17
|
):
|
|
14
18
|
"""Initialize a workspace environment in the current directory."""
|
|
15
19
|
cwd = Path.cwd()
|
|
16
20
|
workspace_config_path = cwd / ".monoco" / "workspace.yaml"
|
|
17
21
|
|
|
18
22
|
if workspace_config_path.exists() and not force:
|
|
19
|
-
|
|
23
|
+
OutputManager.error(f"Workspace already initialized in {cwd}. Use --force to overwrite.")
|
|
20
24
|
raise typer.Exit(code=1)
|
|
21
25
|
|
|
22
26
|
cwd.mkdir(parents=True, exist_ok=True)
|
|
@@ -34,5 +38,9 @@ def init_workspace(
|
|
|
34
38
|
with open(workspace_config_path, "w") as f:
|
|
35
39
|
yaml.dump(config, f, default_flow_style=False)
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
OutputManager.print({
|
|
42
|
+
"status": "initialized",
|
|
43
|
+
"path": str(cwd),
|
|
44
|
+
"config_file": str(workspace_config_path)
|
|
45
|
+
})
|
|
38
46
|
|
monoco/core/agent/adapters.py
CHANGED
|
@@ -21,6 +21,16 @@ class BaseCLIClient:
|
|
|
21
21
|
|
|
22
22
|
def _build_prompt(self, prompt: str, context_files: List[Path]) -> str:
|
|
23
23
|
"""Concatenate prompt and context files."""
|
|
24
|
+
# Inject Language Rule
|
|
25
|
+
try:
|
|
26
|
+
from monoco.core.config import get_config
|
|
27
|
+
settings = get_config()
|
|
28
|
+
lang = settings.i18n.source_lang
|
|
29
|
+
if lang:
|
|
30
|
+
prompt = f"{prompt}\n\n[SYSTEM: LANGUAGE CONSTRAINT]\nThe project source language is '{lang}'. You MUST use '{lang}' for all thinking and reporting unless explicitly instructed otherwise."
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
|
|
24
34
|
full_prompt = [prompt]
|
|
25
35
|
if context_files:
|
|
26
36
|
full_prompt.append("\n\n--- CONTEXT FILES ---")
|
|
@@ -93,10 +103,23 @@ class QwenClient(BaseCLIClient, AgentClient):
|
|
|
93
103
|
return await self._run_command([self._executable, full_prompt])
|
|
94
104
|
|
|
95
105
|
|
|
106
|
+
class KimiClient(BaseCLIClient, AgentClient):
|
|
107
|
+
"""Adapter for Moonshot Kimi CLI."""
|
|
108
|
+
|
|
109
|
+
def __init__(self):
|
|
110
|
+
super().__init__("kimi")
|
|
111
|
+
|
|
112
|
+
async def execute(self, prompt: str, context_files: List[Path] = []) -> str:
|
|
113
|
+
full_prompt = self._build_prompt(prompt, context_files)
|
|
114
|
+
# Usage: kimi "prompt"
|
|
115
|
+
return await self._run_command([self._executable, full_prompt])
|
|
116
|
+
|
|
117
|
+
|
|
96
118
|
_ADAPTERS = {
|
|
97
119
|
"gemini": GeminiClient,
|
|
98
120
|
"claude": ClaudeClient,
|
|
99
|
-
"qwen": QwenClient
|
|
121
|
+
"qwen": QwenClient,
|
|
122
|
+
"kimi": KimiClient
|
|
100
123
|
}
|
|
101
124
|
|
|
102
125
|
def get_agent_client(name: str) -> AgentClient:
|
monoco/core/config.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import yaml
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Optional, Dict, Any, Callable, Awaitable
|
|
4
|
+
from typing import Optional, Dict, Any, Callable, Awaitable, List
|
|
5
5
|
from enum import Enum
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
@@ -23,7 +23,6 @@ class PathsConfig(BaseModel):
|
|
|
23
23
|
|
|
24
24
|
class CoreConfig(BaseModel):
|
|
25
25
|
"""Core system configuration."""
|
|
26
|
-
editor: str = Field(default_factory=lambda: os.getenv("EDITOR", "vim"), description="Preferred text editor")
|
|
27
26
|
log_level: str = Field(default="INFO", description="Logging verbosity")
|
|
28
27
|
author: Optional[str] = Field(default=None, description="Default author for new artifacts")
|
|
29
28
|
|
|
@@ -57,6 +56,67 @@ class AgentConfig(BaseModel):
|
|
|
57
56
|
description="Custom agent framework integrations (overrides defaults from monoco.core.integrations)"
|
|
58
57
|
)
|
|
59
58
|
|
|
59
|
+
class IssueTypeConfig(BaseModel):
|
|
60
|
+
name: str
|
|
61
|
+
label: str
|
|
62
|
+
prefix: str
|
|
63
|
+
folder: str
|
|
64
|
+
description: Optional[str] = None
|
|
65
|
+
|
|
66
|
+
class TransitionConfig(BaseModel):
|
|
67
|
+
name: str
|
|
68
|
+
label: str
|
|
69
|
+
icon: Optional[str] = None
|
|
70
|
+
from_status: Optional[str] = None
|
|
71
|
+
from_stage: Optional[str] = None
|
|
72
|
+
to_status: str
|
|
73
|
+
to_stage: Optional[str] = None
|
|
74
|
+
required_solution: Optional[str] = None
|
|
75
|
+
description: str = ""
|
|
76
|
+
command_template: Optional[str] = None
|
|
77
|
+
|
|
78
|
+
class IssueSchemaConfig(BaseModel):
|
|
79
|
+
types: List[IssueTypeConfig] = Field(default_factory=list)
|
|
80
|
+
statuses: List[str] = Field(default_factory=list)
|
|
81
|
+
stages: List[str] = Field(default_factory=list)
|
|
82
|
+
solutions: List[str] = Field(default_factory=list)
|
|
83
|
+
workflows: List[TransitionConfig] = Field(default_factory=list)
|
|
84
|
+
|
|
85
|
+
def merge(self, other: "IssueSchemaConfig") -> "IssueSchemaConfig":
|
|
86
|
+
if not other:
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
# Types: merge by name
|
|
90
|
+
if other.types:
|
|
91
|
+
type_map = {t.name: t for t in self.types}
|
|
92
|
+
for ot in other.types:
|
|
93
|
+
type_map[ot.name] = ot
|
|
94
|
+
self.types = list(type_map.values())
|
|
95
|
+
|
|
96
|
+
# Statuses: replace if provided
|
|
97
|
+
if other.statuses:
|
|
98
|
+
self.statuses = other.statuses
|
|
99
|
+
|
|
100
|
+
# Stages: replace if provided
|
|
101
|
+
if other.stages:
|
|
102
|
+
self.stages = other.stages
|
|
103
|
+
|
|
104
|
+
# Solutions: replace if provided
|
|
105
|
+
if other.solutions:
|
|
106
|
+
self.solutions = other.solutions
|
|
107
|
+
|
|
108
|
+
# Workflows (Transitions): merge by name
|
|
109
|
+
if other.workflows:
|
|
110
|
+
wf_map = {w.name: w for w in self.workflows}
|
|
111
|
+
for ow in other.workflows:
|
|
112
|
+
wf_map[ow.name] = ow
|
|
113
|
+
self.workflows = list(wf_map.values())
|
|
114
|
+
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
class StateMachineConfig(BaseModel):
|
|
118
|
+
transitions: List[TransitionConfig]
|
|
119
|
+
|
|
60
120
|
class MonocoConfig(BaseModel):
|
|
61
121
|
"""
|
|
62
122
|
Main Configuration Schema.
|
|
@@ -69,6 +129,7 @@ class MonocoConfig(BaseModel):
|
|
|
69
129
|
ui: UIConfig = Field(default_factory=UIConfig)
|
|
70
130
|
telemetry: TelemetryConfig = Field(default_factory=TelemetryConfig)
|
|
71
131
|
agent: AgentConfig = Field(default_factory=AgentConfig)
|
|
132
|
+
issue: IssueSchemaConfig = Field(default_factory=IssueSchemaConfig)
|
|
72
133
|
|
|
73
134
|
@staticmethod
|
|
74
135
|
def _deep_merge(base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -165,14 +226,31 @@ def get_config(project_root: Optional[str] = None, require_project: bool = False
|
|
|
165
226
|
class ConfigScope(str, Enum):
|
|
166
227
|
GLOBAL = "global"
|
|
167
228
|
PROJECT = "project"
|
|
229
|
+
WORKSPACE = "workspace"
|
|
168
230
|
|
|
169
231
|
def get_config_path(scope: ConfigScope, project_root: Optional[str] = None) -> Path:
|
|
170
232
|
"""Get the path to the configuration file for a given scope."""
|
|
171
233
|
if scope == ConfigScope.GLOBAL:
|
|
172
234
|
return Path.home() / ".monoco" / "config.yaml"
|
|
235
|
+
elif scope == ConfigScope.WORKSPACE:
|
|
236
|
+
cwd = Path(project_root) if project_root else Path.cwd()
|
|
237
|
+
return cwd / ".monoco" / "workspace.yaml"
|
|
173
238
|
else:
|
|
239
|
+
# ConfigScope.PROJECT
|
|
174
240
|
cwd = Path(project_root) if project_root else Path.cwd()
|
|
175
|
-
return cwd / ".monoco" / "
|
|
241
|
+
return cwd / ".monoco" / "project.yaml"
|
|
242
|
+
|
|
243
|
+
def find_monoco_root(start_path: Optional[Path] = None) -> Path:
|
|
244
|
+
"""Recursively find the .monoco directory upwards."""
|
|
245
|
+
current = (start_path or Path.cwd()).resolve()
|
|
246
|
+
# Check if we are inside a .monoco folder (unlikely but possible)
|
|
247
|
+
if current.name == ".monoco":
|
|
248
|
+
return current.parent
|
|
249
|
+
|
|
250
|
+
for parent in [current] + list(current.parents):
|
|
251
|
+
if (parent / ".monoco").exists():
|
|
252
|
+
return parent
|
|
253
|
+
return current
|
|
176
254
|
|
|
177
255
|
def load_raw_config(scope: ConfigScope, project_root: Optional[str] = None) -> Dict[str, Any]:
|
|
178
256
|
"""Load raw configuration dictionary from a specific scope."""
|
monoco/core/integrations.py
CHANGED
|
@@ -110,6 +110,14 @@ DEFAULT_INTEGRATIONS: Dict[str, AgentIntegration] = {
|
|
|
110
110
|
bin_name="qwen",
|
|
111
111
|
version_cmd="--version",
|
|
112
112
|
),
|
|
113
|
+
"kimi": AgentIntegration(
|
|
114
|
+
key="kimi",
|
|
115
|
+
name="Kimi CLI",
|
|
116
|
+
system_prompt_file="KIMI.md",
|
|
117
|
+
skill_root_dir=".kimi/skills/",
|
|
118
|
+
bin_name="kimi",
|
|
119
|
+
version_cmd="--version",
|
|
120
|
+
),
|
|
113
121
|
"agent": AgentIntegration(
|
|
114
122
|
key="agent",
|
|
115
123
|
name="Antigravity",
|
monoco/core/lsp.py
CHANGED
|
@@ -24,6 +24,13 @@ class Range(BaseModel):
|
|
|
24
24
|
def __repr__(self):
|
|
25
25
|
return f"{self.start.line}:{self.start.character}-{self.end.line}:{self.end.character}"
|
|
26
26
|
|
|
27
|
+
class Location(BaseModel):
|
|
28
|
+
"""
|
|
29
|
+
Represents a location inside a resource, such as a line of code inside a text file.
|
|
30
|
+
"""
|
|
31
|
+
uri: str
|
|
32
|
+
range: Range
|
|
33
|
+
|
|
27
34
|
class DiagnosticSeverity(IntEnum):
|
|
28
35
|
Error = 1
|
|
29
36
|
Warning = 2
|
monoco/core/output.py
CHANGED
|
@@ -63,8 +63,15 @@ class OutputManager:
|
|
|
63
63
|
print(json.dumps([item.model_dump(mode='json', exclude_none=True) for item in data], separators=(',', ':')))
|
|
64
64
|
else:
|
|
65
65
|
# Fallback for dicts/lists/primitives
|
|
66
|
+
def _encoder(obj):
|
|
67
|
+
if isinstance(obj, BaseModel):
|
|
68
|
+
return obj.model_dump(mode='json', exclude_none=True)
|
|
69
|
+
if hasattr(obj, 'value'): # Enum support
|
|
70
|
+
return obj.value
|
|
71
|
+
return str(obj)
|
|
72
|
+
|
|
66
73
|
try:
|
|
67
|
-
print(json.dumps(data, separators=(',', ':'), default=
|
|
74
|
+
print(json.dumps(data, separators=(',', ':'), default=_encoder))
|
|
68
75
|
except TypeError:
|
|
69
76
|
print(str(data))
|
|
70
77
|
|
|
@@ -52,7 +52,7 @@ Configuration is stored in YAML format at:
|
|
|
52
52
|
|
|
53
53
|
Key configuration sections:
|
|
54
54
|
|
|
55
|
-
- `core`:
|
|
55
|
+
- `core`: Log level, author
|
|
56
56
|
- `paths`: Directory paths (issues, spikes, specs)
|
|
57
57
|
- `project`: Project metadata, spike repos, workspace members
|
|
58
58
|
- `i18n`: Internationalization settings
|
monoco/core/setup.py
CHANGED
|
@@ -162,7 +162,6 @@ def init_cli(
|
|
|
162
162
|
user_config = {
|
|
163
163
|
"core": {
|
|
164
164
|
"author": author,
|
|
165
|
-
# Editor is handled by env/config defaults, no need to prompt
|
|
166
165
|
},
|
|
167
166
|
"telemetry": {
|
|
168
167
|
"enabled": telemetry
|
|
@@ -259,6 +258,14 @@ def init_cli(
|
|
|
259
258
|
(cwd / "Issues").mkdir(exist_ok=True)
|
|
260
259
|
(cwd / ".references").mkdir(exist_ok=True)
|
|
261
260
|
|
|
261
|
+
# Initialize Agent Resources
|
|
262
|
+
try:
|
|
263
|
+
from monoco.features.agent.core import init_agent_resources
|
|
264
|
+
init_agent_resources(cwd)
|
|
265
|
+
console.print(f"[dim] - Agent Resources: .monoco/actions/*.prompty[/dim]")
|
|
266
|
+
except Exception as e:
|
|
267
|
+
console.print(f"[yellow]Warning: Failed to init agent resources: {e}[/yellow]")
|
|
268
|
+
|
|
262
269
|
console.print("\n[bold green]✓ Monoco Project Initialized![/bold green]")
|
|
263
270
|
console.print(f"Access configured! issues will be created as [bold]{project_key}-XXX[/bold]")
|
|
264
271
|
|
monoco/daemon/app.py
CHANGED
|
@@ -9,7 +9,6 @@ from typing import Optional, List, Dict
|
|
|
9
9
|
from monoco.daemon.services import Broadcaster, ProjectManager
|
|
10
10
|
from monoco.core.git import GitMonitor
|
|
11
11
|
from monoco.core.config import get_config, ConfigMonitor, ConfigScope, get_config_path
|
|
12
|
-
from fastapi import FastAPI, Request, HTTPException, Query
|
|
13
12
|
|
|
14
13
|
# Configure logging
|
|
15
14
|
logging.basicConfig(level=logging.INFO)
|
|
@@ -29,7 +28,7 @@ Monoco Daemon Process
|
|
|
29
28
|
# Service Instances
|
|
30
29
|
broadcaster = Broadcaster()
|
|
31
30
|
git_monitor: GitMonitor | None = None
|
|
32
|
-
|
|
31
|
+
config_monitors: List[ConfigMonitor] = []
|
|
33
32
|
project_manager: ProjectManager | None = None
|
|
34
33
|
|
|
35
34
|
@asynccontextmanager
|
|
@@ -37,7 +36,7 @@ async def lifespan(app: FastAPI):
|
|
|
37
36
|
# Startup
|
|
38
37
|
logger.info("Starting Monoco Daemon services...")
|
|
39
38
|
|
|
40
|
-
global project_manager, git_monitor,
|
|
39
|
+
global project_manager, git_monitor, config_monitors
|
|
41
40
|
# Use MONOCO_SERVER_ROOT if set, otherwise CWD
|
|
42
41
|
env_root = os.getenv("MONOCO_SERVER_ROOT")
|
|
43
42
|
workspace_root = Path(env_root) if env_root else Path.cwd()
|
|
@@ -50,32 +49,39 @@ async def lifespan(app: FastAPI):
|
|
|
50
49
|
"hash": new_hash
|
|
51
50
|
})
|
|
52
51
|
|
|
53
|
-
async def on_config_change():
|
|
54
|
-
logger.info("Config file changed, broadcasting update...")
|
|
52
|
+
async def on_config_change(path: str):
|
|
53
|
+
logger.info(f"Config file changed: {path}, broadcasting update...")
|
|
55
54
|
await broadcaster.broadcast("CONFIG_UPDATED", {
|
|
56
55
|
"scope": "workspace",
|
|
57
|
-
"path":
|
|
56
|
+
"path": path
|
|
58
57
|
})
|
|
59
58
|
|
|
60
59
|
git_monitor = GitMonitor(workspace_root, on_git_change)
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
project_config_path = get_config_path(ConfigScope.PROJECT, str(workspace_root))
|
|
62
|
+
workspace_config_path = get_config_path(ConfigScope.WORKSPACE, str(workspace_root))
|
|
63
|
+
|
|
64
|
+
config_monitors = [
|
|
65
|
+
ConfigMonitor(project_config_path, lambda: on_config_change(str(project_config_path))),
|
|
66
|
+
ConfigMonitor(workspace_config_path, lambda: on_config_change(str(workspace_config_path)))
|
|
67
|
+
]
|
|
64
68
|
|
|
65
69
|
await project_manager.start_all()
|
|
66
70
|
git_task = asyncio.create_task(git_monitor.start())
|
|
67
|
-
|
|
71
|
+
config_tasks = [asyncio.create_task(m.start()) for m in config_monitors]
|
|
68
72
|
|
|
69
73
|
yield
|
|
70
74
|
# Shutdown
|
|
71
75
|
logger.info("Shutting down Monoco Daemon services...")
|
|
72
76
|
if git_monitor:
|
|
73
77
|
git_monitor.stop()
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
for m in config_monitors:
|
|
79
|
+
m.stop()
|
|
76
80
|
if project_manager:
|
|
77
81
|
project_manager.stop_all()
|
|
78
|
-
|
|
82
|
+
|
|
83
|
+
await git_task
|
|
84
|
+
await asyncio.gather(*config_tasks)
|
|
79
85
|
|
|
80
86
|
app = FastAPI(
|
|
81
87
|
title="Monoco Daemon",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
import typer
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import Optional, Annotated
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from monoco.core.output import print_output, print_error
|
|
5
|
+
from monoco.core.output import print_output, print_error, AgentOutput, OutputManager
|
|
6
6
|
from monoco.core.agent.adapters import get_agent_client
|
|
7
7
|
from monoco.core.agent.state import AgentStateManager
|
|
8
8
|
from monoco.core.agent.action import ActionRegistry, ActionContext
|
|
@@ -12,6 +12,8 @@ import re
|
|
|
12
12
|
import json as j
|
|
13
13
|
|
|
14
14
|
app = typer.Typer()
|
|
15
|
+
action_app = typer.Typer(name="action", help="Manage generic agent actions/prompts.")
|
|
16
|
+
app.add_typer(action_app)
|
|
15
17
|
|
|
16
18
|
@app.command(name="run")
|
|
17
19
|
def run_command(
|
|
@@ -19,6 +21,7 @@ def run_command(
|
|
|
19
21
|
target: Optional[str] = typer.Argument(None, help="Target file argument for the task"),
|
|
20
22
|
provider: Optional[str] = typer.Option(None, "--using", "-u", help="Override agent provider"),
|
|
21
23
|
instruction: Optional[str] = typer.Option(None, "--instruction", "-i", help="Additional instruction for the agent"),
|
|
24
|
+
json: AgentOutput = False,
|
|
22
25
|
):
|
|
23
26
|
"""
|
|
24
27
|
Execute a prompt or a named task using an Agent CLI.
|
|
@@ -39,7 +42,8 @@ def run_command(
|
|
|
39
42
|
|
|
40
43
|
if action:
|
|
41
44
|
# It IS an action
|
|
42
|
-
|
|
45
|
+
if not OutputManager.is_agent_mode():
|
|
46
|
+
print(f"Running action: {action.name}")
|
|
43
47
|
|
|
44
48
|
# Simple template substitution
|
|
45
49
|
final_prompt = action.template
|
|
@@ -72,7 +76,8 @@ def run_command(
|
|
|
72
76
|
# 3. State Check
|
|
73
77
|
state = state_manager.load()
|
|
74
78
|
if not state or state.is_stale:
|
|
75
|
-
|
|
79
|
+
if not OutputManager.is_agent_mode():
|
|
80
|
+
print("Agent state stale or missing, refreshing...")
|
|
76
81
|
state = state_manager.refresh()
|
|
77
82
|
|
|
78
83
|
if prov_name not in state.providers:
|
|
@@ -87,15 +92,19 @@ def run_command(
|
|
|
87
92
|
try:
|
|
88
93
|
client = get_agent_client(prov_name)
|
|
89
94
|
result = asyncio.run(client.execute(final_prompt, context_files=context_files))
|
|
90
|
-
|
|
95
|
+
|
|
96
|
+
if OutputManager.is_agent_mode():
|
|
97
|
+
OutputManager.print({"result": result, "provider": prov_name})
|
|
98
|
+
else:
|
|
99
|
+
print(result)
|
|
91
100
|
|
|
92
101
|
except Exception as e:
|
|
93
102
|
print_error(f"Execution failed: {e}")
|
|
94
103
|
raise typer.Exit(1)
|
|
95
104
|
|
|
96
|
-
@
|
|
97
|
-
def
|
|
98
|
-
json:
|
|
105
|
+
@action_app.command(name="list")
|
|
106
|
+
def action_list(
|
|
107
|
+
json: AgentOutput = False,
|
|
99
108
|
context: Optional[str] = typer.Option(None, "--context", help="Context for filtering (JSON string)")
|
|
100
109
|
):
|
|
101
110
|
"""List available actions."""
|
|
@@ -111,29 +120,78 @@ def list(
|
|
|
111
120
|
print_error(f"Invalid context JSON: {e}")
|
|
112
121
|
|
|
113
122
|
actions = registry.list_available(action_context)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
|
|
124
|
+
# Filter by State Machine if context implies an Issue
|
|
125
|
+
if action_context and (action_context.status or action_context.stage):
|
|
126
|
+
try:
|
|
127
|
+
from monoco.features.issue.engine.machine import StateMachine
|
|
128
|
+
from monoco.features.issue.models import IssueMetadata
|
|
129
|
+
|
|
130
|
+
cfg = settings.issue
|
|
131
|
+
sm = StateMachine(cfg)
|
|
132
|
+
|
|
133
|
+
# Create a mock metadata for state check
|
|
134
|
+
mock_meta = IssueMetadata(
|
|
135
|
+
id="mock-id",
|
|
136
|
+
title="Mock Issue",
|
|
137
|
+
type=action_context.type or "feature",
|
|
138
|
+
status=action_context.status or "open",
|
|
139
|
+
stage=action_context.stage
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
allowed_transitions = sm.get_available_transitions(mock_meta)
|
|
143
|
+
allowed_names = {t.name for t in allowed_transitions}
|
|
144
|
+
|
|
145
|
+
# Identify which actions are actually transitions
|
|
146
|
+
all_transition_names = {t.name for t in cfg.workflows}
|
|
147
|
+
|
|
148
|
+
filtered_actions = []
|
|
149
|
+
for action in actions:
|
|
150
|
+
# If an action is a formal transition, it must be allowed
|
|
151
|
+
if action.name in all_transition_names:
|
|
152
|
+
if action.name in allowed_names:
|
|
153
|
+
filtered_actions.append(action)
|
|
154
|
+
else:
|
|
155
|
+
# Generic actions are passed through (already filtered by 'when')
|
|
156
|
+
filtered_actions.append(action)
|
|
157
|
+
|
|
158
|
+
actions = filtered_actions
|
|
159
|
+
|
|
160
|
+
except Exception:
|
|
161
|
+
# Fallback if Issue feature is missing or context is invalid
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
# OutputManager handles list of Pydantic models automatically for both JSON and Table
|
|
165
|
+
print_output(actions, title="Available Actions")
|
|
166
|
+
|
|
167
|
+
@app.command(name="list")
|
|
168
|
+
def list_providers(
|
|
169
|
+
json: AgentOutput = False,
|
|
170
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force refresh of agent state")
|
|
171
|
+
):
|
|
172
|
+
"""List available agent providers and their status."""
|
|
173
|
+
# Reuse status logic
|
|
174
|
+
status(json=json, force=force)
|
|
118
175
|
|
|
119
176
|
@app.command()
|
|
120
177
|
def status(
|
|
121
|
-
json:
|
|
178
|
+
json: AgentOutput = False,
|
|
122
179
|
force: bool = typer.Option(False, "--force", "-f", help="Force refresh of agent state")
|
|
123
180
|
):
|
|
124
181
|
"""View status of Agent Providers."""
|
|
125
182
|
state_manager = AgentStateManager()
|
|
126
183
|
state = state_manager.get_or_refresh(force=force)
|
|
127
184
|
|
|
128
|
-
if
|
|
129
|
-
import json as j
|
|
185
|
+
if OutputManager.is_agent_mode():
|
|
130
186
|
# Convert datetime to ISO string for JSON serialization
|
|
131
187
|
data = state.dict()
|
|
132
188
|
data["last_checked"] = data["last_checked"].isoformat()
|
|
133
|
-
print(
|
|
189
|
+
OutputManager.print(data)
|
|
134
190
|
else:
|
|
135
191
|
# Standard output using existing print_output or custom formatting
|
|
136
192
|
from monoco.core.output import Table
|
|
193
|
+
from rich import print as rprint
|
|
194
|
+
|
|
137
195
|
table = Table(title=f"Agent Status (Last Checked: {state.last_checked.strftime('%Y-%m-%d %H:%M:%S')})")
|
|
138
196
|
table.add_column("Provider")
|
|
139
197
|
table.add_column("Available")
|
|
@@ -147,7 +205,7 @@ def status(
|
|
|
147
205
|
p_state.path or "-",
|
|
148
206
|
p_state.error or "-"
|
|
149
207
|
)
|
|
150
|
-
|
|
208
|
+
rprint(table)
|
|
151
209
|
|
|
152
210
|
@app.command()
|
|
153
211
|
def doctor(
|
|
@@ -158,3 +216,22 @@ def doctor(
|
|
|
158
216
|
"""
|
|
159
217
|
from monoco.features.agent.doctor import doctor as doc_impl
|
|
160
218
|
doc_impl(force)
|
|
219
|
+
|
|
220
|
+
@app.command()
|
|
221
|
+
def init(
|
|
222
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force overwrite existing resources")
|
|
223
|
+
):
|
|
224
|
+
"""
|
|
225
|
+
Initialize Agent Resources (prompts) in the current project.
|
|
226
|
+
"""
|
|
227
|
+
settings = get_config()
|
|
228
|
+
root = Path(settings.paths.root)
|
|
229
|
+
|
|
230
|
+
from monoco.features.agent.core import init_agent_resources
|
|
231
|
+
# TODO: Pass force arg when supported by core
|
|
232
|
+
init_agent_resources(root)
|
|
233
|
+
|
|
234
|
+
if OutputManager.is_agent_mode():
|
|
235
|
+
OutputManager.print({"status": "initialized"})
|
|
236
|
+
else:
|
|
237
|
+
print(f"Agent resources initialized in {root}")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List
|
|
5
|
+
try:
|
|
6
|
+
from importlib.resources import files
|
|
7
|
+
except ImportError:
|
|
8
|
+
# Fallback for python < 3.9
|
|
9
|
+
from importlib_resources import files
|
|
10
|
+
|
|
11
|
+
def init_agent_resources(project_root: Path):
|
|
12
|
+
"""
|
|
13
|
+
Initialize Agent resources (prompts) in the project workspace.
|
|
14
|
+
"""
|
|
15
|
+
actions_dir = project_root / ".monoco" / "actions"
|
|
16
|
+
actions_dir.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
|
|
18
|
+
# Determine language from config, default to 'en'
|
|
19
|
+
# We need to load config to know the language.
|
|
20
|
+
# But this function might be called before full config load?
|
|
21
|
+
# Helper to peek at config or default to 'en'
|
|
22
|
+
from monoco.core.config import get_config
|
|
23
|
+
try:
|
|
24
|
+
settings = get_config(str(project_root), require_project=False) # Best effort
|
|
25
|
+
lang = settings.core.language or "en"
|
|
26
|
+
except Exception:
|
|
27
|
+
lang = "en"
|
|
28
|
+
|
|
29
|
+
# Define source path relative to this module
|
|
30
|
+
try:
|
|
31
|
+
current_dir = Path(__file__).parent
|
|
32
|
+
|
|
33
|
+
# Try specific language, fallback to 'en'
|
|
34
|
+
source_dir = current_dir / "resources" / lang
|
|
35
|
+
if not source_dir.exists():
|
|
36
|
+
source_dir = current_dir / "resources" / "en"
|
|
37
|
+
|
|
38
|
+
if source_dir.exists():
|
|
39
|
+
for item in source_dir.glob("*.prompty"):
|
|
40
|
+
target = actions_dir / item.name
|
|
41
|
+
# Copy if not exists, or overwrite?
|
|
42
|
+
# Ideally init should be safe to run multiple times (idempotent)
|
|
43
|
+
# But user might have customized them.
|
|
44
|
+
if not target.exists():
|
|
45
|
+
shutil.copy2(item, target)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
# Fallback or Log?
|
|
48
|
+
pass
|