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/cli/project.py
CHANGED
|
@@ -1,35 +1,33 @@
|
|
|
1
1
|
import typer
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import Optional
|
|
4
4
|
from rich.console import Console
|
|
5
5
|
from rich.table import Table
|
|
6
6
|
import yaml
|
|
7
|
-
import json
|
|
8
|
-
import os
|
|
9
7
|
|
|
10
|
-
from monoco.core.workspace import find_projects
|
|
11
|
-
from monoco.core.config import get_config
|
|
8
|
+
from monoco.core.workspace import find_projects
|
|
12
9
|
from monoco.core.output import AgentOutput, OutputManager
|
|
13
10
|
|
|
14
11
|
app = typer.Typer(help="Manage Monoco Projects")
|
|
15
12
|
console = Console()
|
|
16
13
|
|
|
14
|
+
|
|
17
15
|
@app.command("list")
|
|
18
16
|
def list_projects(
|
|
19
17
|
json: AgentOutput = False,
|
|
20
|
-
root: Optional[str] = typer.Option(None, "--root", help="Workspace root")
|
|
18
|
+
root: Optional[str] = typer.Option(None, "--root", help="Workspace root"),
|
|
21
19
|
):
|
|
22
20
|
"""List all discovered projects in the workspace."""
|
|
23
21
|
cwd = Path(root).resolve() if root else Path.cwd()
|
|
24
22
|
projects = find_projects(cwd)
|
|
25
|
-
|
|
23
|
+
|
|
26
24
|
if OutputManager.is_agent_mode():
|
|
27
25
|
data = [
|
|
28
26
|
{
|
|
29
27
|
"id": p.id,
|
|
30
28
|
"name": p.name,
|
|
31
29
|
"path": str(p.path),
|
|
32
|
-
"key": p.config.project.key if p.config.project else ""
|
|
30
|
+
"key": p.config.project.key if p.config.project else "",
|
|
33
31
|
}
|
|
34
32
|
for p in projects
|
|
35
33
|
]
|
|
@@ -40,48 +38,54 @@ def list_projects(
|
|
|
40
38
|
table.add_column("Name", style="magenta")
|
|
41
39
|
table.add_column("Key", style="green")
|
|
42
40
|
table.add_column("Path", style="dim")
|
|
43
|
-
|
|
41
|
+
|
|
44
42
|
for p in projects:
|
|
45
|
-
path_str =
|
|
43
|
+
path_str = (
|
|
44
|
+
str(p.path.relative_to(cwd))
|
|
45
|
+
if p.path.is_relative_to(cwd)
|
|
46
|
+
else str(p.path)
|
|
47
|
+
)
|
|
46
48
|
if path_str == ".":
|
|
47
49
|
path_str = "(root)"
|
|
48
50
|
key = p.config.project.key if p.config.project else "N/A"
|
|
49
51
|
table.add_row(p.id, p.name, key, path_str)
|
|
50
|
-
|
|
52
|
+
|
|
51
53
|
console.print(table)
|
|
52
54
|
|
|
55
|
+
|
|
53
56
|
@app.command("init")
|
|
54
57
|
def init_project(
|
|
55
58
|
name: str = typer.Option(..., "--name", "-n", help="Project Name"),
|
|
56
59
|
key: str = typer.Option(..., "--key", "-k", help="Project Key"),
|
|
57
|
-
force: bool = typer.Option(
|
|
60
|
+
force: bool = typer.Option(
|
|
61
|
+
False, "--force", "-f", help="Overwrite existing config"
|
|
62
|
+
),
|
|
58
63
|
json: AgentOutput = False,
|
|
59
64
|
):
|
|
60
65
|
"""Initialize a new project in the current directory."""
|
|
61
66
|
cwd = Path.cwd()
|
|
62
67
|
project_config_path = cwd / ".monoco" / "project.yaml"
|
|
63
|
-
|
|
68
|
+
|
|
64
69
|
if project_config_path.exists() and not force:
|
|
65
|
-
OutputManager.error(
|
|
70
|
+
OutputManager.error(
|
|
71
|
+
f"Project already initialized in {cwd}. Use --force to overwrite."
|
|
72
|
+
)
|
|
66
73
|
raise typer.Exit(code=1)
|
|
67
|
-
|
|
74
|
+
|
|
68
75
|
cwd.mkdir(parents=True, exist_ok=True)
|
|
69
76
|
(cwd / ".monoco").mkdir(exist_ok=True)
|
|
70
|
-
|
|
71
|
-
config = {
|
|
72
|
-
|
|
73
|
-
"name": name,
|
|
74
|
-
"key": key
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
77
|
+
|
|
78
|
+
config = {"project": {"name": name, "key": key}}
|
|
79
|
+
|
|
78
80
|
with open(project_config_path, "w") as f:
|
|
79
81
|
yaml.dump(config, f, default_flow_style=False)
|
|
80
|
-
|
|
81
|
-
OutputManager.print(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
|
|
83
|
+
OutputManager.print(
|
|
84
|
+
{
|
|
85
|
+
"status": "initialized",
|
|
86
|
+
"name": name,
|
|
87
|
+
"key": key,
|
|
88
|
+
"path": str(cwd),
|
|
89
|
+
"config_file": str(project_config_path),
|
|
90
|
+
}
|
|
91
|
+
)
|
monoco/cli/workspace.py
CHANGED
|
@@ -1,46 +1,56 @@
|
|
|
1
1
|
import typer
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from rich.console import Console
|
|
4
|
-
from typing import Annotated
|
|
5
4
|
import yaml
|
|
6
|
-
import json
|
|
7
5
|
|
|
8
6
|
from monoco.core.output import AgentOutput, OutputManager
|
|
7
|
+
from monoco.core.hooks import install_hooks
|
|
9
8
|
|
|
10
9
|
app = typer.Typer(help="Manage Monoco Workspace")
|
|
11
10
|
console = Console()
|
|
12
11
|
|
|
12
|
+
|
|
13
13
|
@app.command("init")
|
|
14
14
|
def init_workspace(
|
|
15
|
-
force: bool = typer.Option(
|
|
15
|
+
force: bool = typer.Option(
|
|
16
|
+
False, "--force", "-f", help="Overwrite existing config"
|
|
17
|
+
),
|
|
16
18
|
json: AgentOutput = False,
|
|
17
19
|
):
|
|
18
20
|
"""Initialize a workspace environment in the current directory."""
|
|
19
21
|
cwd = Path.cwd()
|
|
20
22
|
workspace_config_path = cwd / ".monoco" / "workspace.yaml"
|
|
21
|
-
|
|
23
|
+
|
|
22
24
|
if workspace_config_path.exists() and not force:
|
|
23
|
-
OutputManager.error(
|
|
25
|
+
OutputManager.error(
|
|
26
|
+
f"Workspace already initialized in {cwd}. Use --force to overwrite."
|
|
27
|
+
)
|
|
24
28
|
raise typer.Exit(code=1)
|
|
25
29
|
|
|
26
30
|
cwd.mkdir(parents=True, exist_ok=True)
|
|
27
31
|
(cwd / ".monoco").mkdir(exist_ok=True)
|
|
28
|
-
|
|
32
|
+
|
|
29
33
|
# Default workspace config
|
|
30
34
|
config = {
|
|
31
35
|
"paths": {
|
|
32
|
-
"issues": "Issues",
|
|
36
|
+
"issues": "Issues", # Default
|
|
33
37
|
"spikes": ".references",
|
|
34
|
-
|
|
35
|
-
}
|
|
38
|
+
},
|
|
39
|
+
"hooks": {"pre-commit": "monoco issue lint --recursive"},
|
|
36
40
|
}
|
|
37
|
-
|
|
41
|
+
|
|
38
42
|
with open(workspace_config_path, "w") as f:
|
|
39
43
|
yaml.dump(config, f, default_flow_style=False)
|
|
40
|
-
|
|
41
|
-
OutputManager.print({
|
|
42
|
-
"status": "initialized",
|
|
43
|
-
"path": str(cwd),
|
|
44
|
-
"config_file": str(workspace_config_path)
|
|
45
|
-
})
|
|
46
44
|
|
|
45
|
+
try:
|
|
46
|
+
install_hooks(cwd, config["hooks"])
|
|
47
|
+
except Exception as e:
|
|
48
|
+
OutputManager.warning(f"Failed to install hooks: {e}")
|
|
49
|
+
|
|
50
|
+
OutputManager.print(
|
|
51
|
+
{
|
|
52
|
+
"status": "initialized",
|
|
53
|
+
"path": str(cwd),
|
|
54
|
+
"config_file": str(workspace_config_path),
|
|
55
|
+
}
|
|
56
|
+
)
|
monoco/core/agent/__init__.py
CHANGED
monoco/core/agent/action.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
|
|
2
1
|
import re
|
|
3
2
|
import yaml
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
from typing import Dict, List, Optional, Any
|
|
6
|
-
from pydantic import BaseModel
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
7
|
|
|
8
8
|
class ActionContext(BaseModel):
|
|
9
9
|
"""Context information for matching actions."""
|
|
10
|
+
|
|
10
11
|
id: Optional[str] = None
|
|
11
12
|
type: Optional[str] = None
|
|
12
13
|
stage: Optional[str] = None
|
|
@@ -14,8 +15,10 @@ class ActionContext(BaseModel):
|
|
|
14
15
|
file_path: Optional[str] = None
|
|
15
16
|
project_id: Optional[str] = None
|
|
16
17
|
|
|
18
|
+
|
|
17
19
|
class ActionWhen(BaseModel):
|
|
18
20
|
"""Conditions under which an action should be displayed/active."""
|
|
21
|
+
|
|
19
22
|
idMatch: Optional[str] = None
|
|
20
23
|
typeMatch: Optional[str] = None
|
|
21
24
|
stageMatch: Optional[str] = None
|
|
@@ -26,16 +29,33 @@ class ActionWhen(BaseModel):
|
|
|
26
29
|
"""Evaluate if the context matches these criteria."""
|
|
27
30
|
if self.idMatch and context.id and not re.match(self.idMatch, context.id):
|
|
28
31
|
return False
|
|
29
|
-
if
|
|
32
|
+
if (
|
|
33
|
+
self.typeMatch
|
|
34
|
+
and context.type
|
|
35
|
+
and not re.match(self.typeMatch, context.type)
|
|
36
|
+
):
|
|
30
37
|
return False
|
|
31
|
-
if
|
|
38
|
+
if (
|
|
39
|
+
self.stageMatch
|
|
40
|
+
and context.stage
|
|
41
|
+
and not re.match(self.stageMatch, context.stage)
|
|
42
|
+
):
|
|
32
43
|
return False
|
|
33
|
-
if
|
|
44
|
+
if (
|
|
45
|
+
self.statusMatch
|
|
46
|
+
and context.status
|
|
47
|
+
and not re.match(self.statusMatch, context.status)
|
|
48
|
+
):
|
|
34
49
|
return False
|
|
35
|
-
if
|
|
50
|
+
if (
|
|
51
|
+
self.fileMatch
|
|
52
|
+
and context.file_path
|
|
53
|
+
and not re.match(self.fileMatch, context.file_path)
|
|
54
|
+
):
|
|
36
55
|
return False
|
|
37
56
|
return True
|
|
38
57
|
|
|
58
|
+
|
|
39
59
|
class PromptyAction(BaseModel):
|
|
40
60
|
name: str
|
|
41
61
|
description: str
|
|
@@ -46,10 +66,11 @@ class PromptyAction(BaseModel):
|
|
|
46
66
|
outputs: Dict[str, Any] = {}
|
|
47
67
|
template: str
|
|
48
68
|
when: Optional[ActionWhen] = None
|
|
49
|
-
|
|
69
|
+
|
|
50
70
|
# Monoco specific metadata
|
|
51
71
|
path: Optional[str] = None
|
|
52
|
-
provider: Optional[str] = None
|
|
72
|
+
provider: Optional[str] = None # Derived from model.api or explicitly set
|
|
73
|
+
|
|
53
74
|
|
|
54
75
|
class ActionRegistry:
|
|
55
76
|
def __init__(self, project_root: Optional[Path] = None):
|
|
@@ -59,7 +80,7 @@ class ActionRegistry:
|
|
|
59
80
|
def scan(self) -> List[PromptyAction]:
|
|
60
81
|
"""Scan user global and project local directories for .prompty files."""
|
|
61
82
|
self._actions = []
|
|
62
|
-
|
|
83
|
+
|
|
63
84
|
# 1. User Global: ~/.monoco/actions/
|
|
64
85
|
user_dir = Path.home() / ".monoco" / "actions"
|
|
65
86
|
self._scan_dir(user_dir)
|
|
@@ -68,7 +89,7 @@ class ActionRegistry:
|
|
|
68
89
|
if self.project_root:
|
|
69
90
|
project_dir = self.project_root / ".monoco" / "actions"
|
|
70
91
|
self._scan_dir(project_dir)
|
|
71
|
-
|
|
92
|
+
|
|
72
93
|
return self._actions
|
|
73
94
|
|
|
74
95
|
def _scan_dir(self, directory: Path):
|
|
@@ -85,28 +106,29 @@ class ActionRegistry:
|
|
|
85
106
|
|
|
86
107
|
def _load_action(self, file_path: Path) -> Optional[PromptyAction]:
|
|
87
108
|
content = file_path.read_text(encoding="utf-8")
|
|
88
|
-
|
|
109
|
+
|
|
89
110
|
# Prompty Parser (Standard YAML Frontmatter + Body)
|
|
90
111
|
# We look for the first --- and the second ---
|
|
91
112
|
parts = re.split(r"^---\s*$", content, maxsplit=2, flags=re.MULTILINE)
|
|
92
|
-
|
|
113
|
+
|
|
93
114
|
if len(parts) < 3:
|
|
94
115
|
return None
|
|
95
116
|
|
|
96
117
|
frontmatter_raw = parts[1]
|
|
97
118
|
body = parts[2].strip()
|
|
98
|
-
|
|
119
|
+
|
|
99
120
|
try:
|
|
100
121
|
meta = yaml.safe_load(frontmatter_raw)
|
|
101
122
|
if not meta or "name" not in meta:
|
|
102
123
|
# Use filename as fallback name if missing? Prompty usually requires name.
|
|
103
|
-
if not meta:
|
|
124
|
+
if not meta:
|
|
125
|
+
meta = {}
|
|
104
126
|
meta["name"] = meta.get("name", file_path.stem)
|
|
105
127
|
|
|
106
128
|
# Map Prompty 'when' if present
|
|
107
129
|
when_data = meta.get("when")
|
|
108
130
|
when = ActionWhen(**when_data) if when_data else None
|
|
109
|
-
|
|
131
|
+
|
|
110
132
|
action = PromptyAction(
|
|
111
133
|
name=meta["name"],
|
|
112
134
|
description=meta.get("description", ""),
|
|
@@ -118,21 +140,23 @@ class ActionRegistry:
|
|
|
118
140
|
template=body,
|
|
119
141
|
when=when,
|
|
120
142
|
path=str(file_path.absolute()),
|
|
121
|
-
provider=meta.get("provider") or meta.get("model", {}).get("api")
|
|
143
|
+
provider=meta.get("provider") or meta.get("model", {}).get("api"),
|
|
122
144
|
)
|
|
123
145
|
return action
|
|
124
|
-
|
|
146
|
+
|
|
125
147
|
except Exception as e:
|
|
126
148
|
print(f"Invalid Prompty in {file_path}: {e}")
|
|
127
149
|
return None
|
|
128
150
|
|
|
129
|
-
def list_available(
|
|
151
|
+
def list_available(
|
|
152
|
+
self, context: Optional[ActionContext] = None
|
|
153
|
+
) -> List[PromptyAction]:
|
|
130
154
|
if not self._actions:
|
|
131
155
|
self.scan()
|
|
132
|
-
|
|
156
|
+
|
|
133
157
|
if not context:
|
|
134
158
|
return self._actions
|
|
135
|
-
|
|
159
|
+
|
|
136
160
|
return [a for a in self._actions if not a.when or a.when.matches(context)]
|
|
137
161
|
|
|
138
162
|
def get(self, name: str) -> Optional[PromptyAction]:
|
monoco/core/agent/adapters.py
CHANGED
|
@@ -3,11 +3,11 @@ CLI Adapters for Agent Frameworks.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import shutil
|
|
6
|
-
import subprocess
|
|
7
6
|
from typing import List
|
|
8
7
|
from pathlib import Path
|
|
9
8
|
from .protocol import AgentClient
|
|
10
9
|
|
|
10
|
+
|
|
11
11
|
class BaseCLIClient:
|
|
12
12
|
def __init__(self, executable: str):
|
|
13
13
|
self._executable = executable
|
|
@@ -24,10 +24,11 @@ class BaseCLIClient:
|
|
|
24
24
|
# Inject Language Rule
|
|
25
25
|
try:
|
|
26
26
|
from monoco.core.config import get_config
|
|
27
|
+
|
|
27
28
|
settings = get_config()
|
|
28
29
|
lang = settings.i18n.source_lang
|
|
29
30
|
if lang:
|
|
30
|
-
|
|
31
|
+
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
32
|
except Exception:
|
|
32
33
|
pass
|
|
33
34
|
|
|
@@ -39,7 +40,9 @@ class BaseCLIClient:
|
|
|
39
40
|
full_prompt.append(f"\nFile: {file_path}")
|
|
40
41
|
full_prompt.append("```")
|
|
41
42
|
# Read file content safely
|
|
42
|
-
full_prompt.append(
|
|
43
|
+
full_prompt.append(
|
|
44
|
+
file_path.read_text(encoding="utf-8", errors="replace")
|
|
45
|
+
)
|
|
43
46
|
full_prompt.append("```")
|
|
44
47
|
except Exception as e:
|
|
45
48
|
full_prompt.append(f"Error reading {file_path}: {e}")
|
|
@@ -51,25 +54,25 @@ class BaseCLIClient:
|
|
|
51
54
|
# Using synchronous subprocess in async function for now
|
|
52
55
|
# Ideally this should use asyncio.create_subprocess_exec
|
|
53
56
|
import asyncio
|
|
54
|
-
|
|
57
|
+
|
|
55
58
|
proc = await asyncio.create_subprocess_exec(
|
|
56
|
-
*args,
|
|
57
|
-
stdout=asyncio.subprocess.PIPE,
|
|
58
|
-
stderr=asyncio.subprocess.PIPE
|
|
59
|
+
*args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
|
59
60
|
)
|
|
60
|
-
|
|
61
|
+
|
|
61
62
|
stdout, stderr = await proc.communicate()
|
|
62
|
-
|
|
63
|
+
|
|
63
64
|
if proc.returncode != 0:
|
|
64
65
|
error_msg = stderr.decode().strip()
|
|
65
|
-
raise RuntimeError(
|
|
66
|
-
|
|
66
|
+
raise RuntimeError(
|
|
67
|
+
f"Agent CLI failed (code {proc.returncode}): {error_msg}"
|
|
68
|
+
)
|
|
69
|
+
|
|
67
70
|
return stdout.decode().strip()
|
|
68
71
|
|
|
69
72
|
|
|
70
73
|
class GeminiClient(BaseCLIClient, AgentClient):
|
|
71
74
|
"""Adapter for Google Gemini CLI."""
|
|
72
|
-
|
|
75
|
+
|
|
73
76
|
def __init__(self):
|
|
74
77
|
super().__init__("gemini")
|
|
75
78
|
|
|
@@ -81,7 +84,7 @@ class GeminiClient(BaseCLIClient, AgentClient):
|
|
|
81
84
|
|
|
82
85
|
class ClaudeClient(BaseCLIClient, AgentClient):
|
|
83
86
|
"""Adapter for Anthropic Claude CLI."""
|
|
84
|
-
|
|
87
|
+
|
|
85
88
|
def __init__(self):
|
|
86
89
|
super().__init__("claude")
|
|
87
90
|
|
|
@@ -93,7 +96,7 @@ class ClaudeClient(BaseCLIClient, AgentClient):
|
|
|
93
96
|
|
|
94
97
|
class QwenClient(BaseCLIClient, AgentClient):
|
|
95
98
|
"""Adapter for Alibaba Qwen CLI."""
|
|
96
|
-
|
|
99
|
+
|
|
97
100
|
def __init__(self):
|
|
98
101
|
super().__init__("qwen")
|
|
99
102
|
|
|
@@ -105,7 +108,7 @@ class QwenClient(BaseCLIClient, AgentClient):
|
|
|
105
108
|
|
|
106
109
|
class KimiClient(BaseCLIClient, AgentClient):
|
|
107
110
|
"""Adapter for Moonshot Kimi CLI."""
|
|
108
|
-
|
|
111
|
+
|
|
109
112
|
def __init__(self):
|
|
110
113
|
super().__init__("kimi")
|
|
111
114
|
|
|
@@ -119,9 +122,10 @@ _ADAPTERS = {
|
|
|
119
122
|
"gemini": GeminiClient,
|
|
120
123
|
"claude": ClaudeClient,
|
|
121
124
|
"qwen": QwenClient,
|
|
122
|
-
"kimi": KimiClient
|
|
125
|
+
"kimi": KimiClient,
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
|
|
125
129
|
def get_agent_client(name: str) -> AgentClient:
|
|
126
130
|
"""Factory to get agent client by name."""
|
|
127
131
|
if name not in _ADAPTERS:
|
monoco/core/agent/protocol.py
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
Protocol definition for Agent Clients.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from typing import Protocol, List
|
|
5
|
+
from typing import Protocol, List
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
class AgentClient(Protocol):
|
|
9
10
|
"""Protocol for interacting with CLI-based agents."""
|
|
10
|
-
|
|
11
|
+
|
|
11
12
|
@property
|
|
12
13
|
def name(self) -> str:
|
|
13
14
|
"""Name of the agent provider (e.g. 'gemini', 'claude')."""
|
|
@@ -20,11 +21,11 @@ class AgentClient(Protocol):
|
|
|
20
21
|
async def execute(self, prompt: str, context_files: List[Path] = []) -> str:
|
|
21
22
|
"""
|
|
22
23
|
Execute a prompt against the agent.
|
|
23
|
-
|
|
24
|
+
|
|
24
25
|
Args:
|
|
25
26
|
prompt: The main instructions.
|
|
26
27
|
context_files: List of files to provide as context.
|
|
27
|
-
|
|
28
|
+
|
|
28
29
|
Returns:
|
|
29
30
|
The raw string response from the agent.
|
|
30
31
|
"""
|
monoco/core/agent/state.py
CHANGED
|
@@ -5,22 +5,22 @@ Handles persistence and retrieval of agent availability state.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import yaml
|
|
8
|
-
import shutil
|
|
9
|
-
import subprocess
|
|
10
8
|
import logging
|
|
11
9
|
from pathlib import Path
|
|
12
10
|
from datetime import datetime, timezone
|
|
13
11
|
from typing import Dict, Optional
|
|
14
|
-
from pydantic import BaseModel
|
|
12
|
+
from pydantic import BaseModel
|
|
15
13
|
|
|
16
14
|
logger = logging.getLogger("monoco.core.agent.state")
|
|
17
15
|
|
|
16
|
+
|
|
18
17
|
class AgentProviderState(BaseModel):
|
|
19
18
|
available: bool
|
|
20
19
|
path: Optional[str] = None
|
|
21
20
|
error: Optional[str] = None
|
|
22
21
|
latency_ms: Optional[int] = None
|
|
23
22
|
|
|
23
|
+
|
|
24
24
|
class AgentState(BaseModel):
|
|
25
25
|
last_checked: datetime
|
|
26
26
|
providers: Dict[str, AgentProviderState]
|
|
@@ -31,6 +31,7 @@ class AgentState(BaseModel):
|
|
|
31
31
|
delta = datetime.now(timezone.utc) - self.last_checked
|
|
32
32
|
return delta.days > 7
|
|
33
33
|
|
|
34
|
+
|
|
34
35
|
class AgentStateManager:
|
|
35
36
|
def __init__(self, state_path: Path = Path.home() / ".monoco" / "agent_state.yaml"):
|
|
36
37
|
self.state_path = state_path
|
|
@@ -40,7 +41,7 @@ class AgentStateManager:
|
|
|
40
41
|
"""Load state from file, returning None if missing or invalid."""
|
|
41
42
|
if not self.state_path.exists():
|
|
42
43
|
return None
|
|
43
|
-
|
|
44
|
+
|
|
44
45
|
try:
|
|
45
46
|
with open(self.state_path, "r") as f:
|
|
46
47
|
data = yaml.safe_load(f)
|
|
@@ -58,46 +59,45 @@ class AgentStateManager:
|
|
|
58
59
|
self._state = self.load()
|
|
59
60
|
if self._state and not self._state.is_stale:
|
|
60
61
|
return self._state
|
|
61
|
-
|
|
62
|
+
|
|
62
63
|
return self.refresh()
|
|
63
64
|
|
|
64
65
|
def refresh(self) -> AgentState:
|
|
65
66
|
"""Run diagnostics on all integrations and update state."""
|
|
66
67
|
logger.info("Refreshing agent state...")
|
|
67
|
-
|
|
68
|
+
|
|
68
69
|
from monoco.core.integrations import get_all_integrations
|
|
69
70
|
from monoco.core.config import get_config
|
|
70
|
-
|
|
71
|
+
|
|
71
72
|
# Load config to get possible overrides
|
|
72
73
|
# Determine root (hacky for now, should be passed)
|
|
73
74
|
root = Path.cwd()
|
|
74
75
|
config = get_config(str(root))
|
|
75
|
-
|
|
76
|
-
integrations = get_all_integrations(
|
|
77
|
-
|
|
76
|
+
|
|
77
|
+
integrations = get_all_integrations(
|
|
78
|
+
config_overrides=config.agent.integrations, enabled_only=True
|
|
79
|
+
)
|
|
80
|
+
|
|
78
81
|
providers = {}
|
|
79
82
|
for key, integration in integrations.items():
|
|
80
83
|
if not integration.bin_name:
|
|
81
|
-
continue
|
|
82
|
-
|
|
84
|
+
continue # Skip integrations that don't have a binary component
|
|
85
|
+
|
|
83
86
|
health = integration.check_health()
|
|
84
87
|
providers[key] = AgentProviderState(
|
|
85
88
|
available=health.available,
|
|
86
89
|
path=health.path,
|
|
87
90
|
error=health.error,
|
|
88
|
-
latency_ms=health.latency_ms
|
|
91
|
+
latency_ms=health.latency_ms,
|
|
89
92
|
)
|
|
90
|
-
|
|
91
|
-
state = AgentState(
|
|
92
|
-
|
|
93
|
-
providers=providers
|
|
94
|
-
)
|
|
95
|
-
|
|
93
|
+
|
|
94
|
+
state = AgentState(last_checked=datetime.now(timezone.utc), providers=providers)
|
|
95
|
+
|
|
96
96
|
# Save state
|
|
97
97
|
self.state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
98
98
|
with open(self.state_path, "w") as f:
|
|
99
|
-
yaml.dump(state.model_dump(mode=
|
|
100
|
-
|
|
99
|
+
yaml.dump(state.model_dump(mode="json"), f)
|
|
100
|
+
|
|
101
101
|
self._state = state
|
|
102
102
|
return state
|
|
103
103
|
|