monoco-toolkit 0.1.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.
- monoco/core/__init__.py +0 -0
- monoco/core/config.py +113 -0
- monoco/core/git.py +184 -0
- monoco/core/output.py +97 -0
- monoco/core/setup.py +285 -0
- monoco/core/telemetry.py +89 -0
- monoco/core/workspace.py +40 -0
- monoco/daemon/__init__.py +0 -0
- monoco/daemon/app.py +378 -0
- monoco/daemon/commands.py +36 -0
- monoco/daemon/models.py +24 -0
- monoco/daemon/reproduce_stats.py +41 -0
- monoco/daemon/services.py +265 -0
- monoco/daemon/stats.py +124 -0
- monoco/features/__init__.py +0 -0
- monoco/features/config/commands.py +70 -0
- monoco/features/i18n/__init__.py +0 -0
- monoco/features/i18n/commands.py +121 -0
- monoco/features/i18n/core.py +178 -0
- monoco/features/issue/commands.py +710 -0
- monoco/features/issue/core.py +1183 -0
- monoco/features/issue/linter.py +172 -0
- monoco/features/issue/models.py +157 -0
- monoco/features/pty/core.py +185 -0
- monoco/features/pty/router.py +138 -0
- monoco/features/pty/server.py +56 -0
- monoco/features/skills/__init__.py +1 -0
- monoco/features/skills/core.py +96 -0
- monoco/features/spike/commands.py +110 -0
- monoco/features/spike/core.py +154 -0
- monoco/main.py +110 -0
- monoco_toolkit-0.1.5.dist-info/METADATA +93 -0
- monoco_toolkit-0.1.5.dist-info/RECORD +36 -0
- monoco_toolkit-0.1.5.dist-info/WHEEL +4 -0
- monoco_toolkit-0.1.5.dist-info/entry_points.txt +2 -0
- monoco_toolkit-0.1.5.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import signal
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
import uvicorn
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
from monoco.features.pty.router import router as pty_router, pty_manager
|
|
10
|
+
|
|
11
|
+
@asynccontextmanager
|
|
12
|
+
async def lifespan(app: FastAPI):
|
|
13
|
+
# Startup
|
|
14
|
+
yield
|
|
15
|
+
# Shutdown
|
|
16
|
+
logging.info("Shutting down PTY manager and cleaning up sessions...")
|
|
17
|
+
pty_manager.close_all_sessions()
|
|
18
|
+
|
|
19
|
+
def run_pty_server(host: str = "127.0.0.1", port: int = 3124, cwd: Optional[Path] = None):
|
|
20
|
+
"""
|
|
21
|
+
Entry point for the 'monoco pty' command.
|
|
22
|
+
"""
|
|
23
|
+
# Configure Logging
|
|
24
|
+
logging.basicConfig(level=logging.INFO)
|
|
25
|
+
|
|
26
|
+
# Register a manual signal handler to ensure we catch termination even if uvicorn misses it
|
|
27
|
+
# or if we are stuck before uvicorn starts.
|
|
28
|
+
def handle_signal(signum, frame):
|
|
29
|
+
logging.info(f"Received signal {signum}, initiating shutdown...")
|
|
30
|
+
# We rely on uvicorn to handle the actual exit loop for SIGINT/SIGTERM usually,
|
|
31
|
+
# but having this log confirms propagation.
|
|
32
|
+
# If uvicorn is running, it should catch this first.
|
|
33
|
+
# If not, we exit manually.
|
|
34
|
+
sys.exit(0)
|
|
35
|
+
|
|
36
|
+
# Note: Uvicorn overwrites SIGINT/SIGTERM handlers by default.
|
|
37
|
+
# relying on lifespan is the standard "Uvicorn way".
|
|
38
|
+
|
|
39
|
+
app = FastAPI(title="Monoco PTY Service", lifespan=lifespan)
|
|
40
|
+
app.include_router(pty_router)
|
|
41
|
+
|
|
42
|
+
# If cwd is provided, we might want to set it as current process CWD
|
|
43
|
+
# so that new sessions default to it.
|
|
44
|
+
if cwd and cwd.exists():
|
|
45
|
+
import os
|
|
46
|
+
os.chdir(cwd)
|
|
47
|
+
logging.info(f"PTY Service Root: {cwd}")
|
|
48
|
+
|
|
49
|
+
logging.info(f"Starting Monoco PTY Service on ws://{host}:{port}")
|
|
50
|
+
try:
|
|
51
|
+
uvicorn.run(app, host=host, port=port)
|
|
52
|
+
except KeyboardInterrupt:
|
|
53
|
+
pass
|
|
54
|
+
finally:
|
|
55
|
+
# Final safety net
|
|
56
|
+
pty_manager.close_all_sessions()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .core import init
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, List, Any
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
def init(root: Path, resources: List[Dict[str, Any]]):
|
|
10
|
+
"""
|
|
11
|
+
Initialize the Skills module.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
root: Project root directory.
|
|
15
|
+
resources: List of resource dicts from modules.
|
|
16
|
+
Expected format:
|
|
17
|
+
{
|
|
18
|
+
"skills": { "name": "content" },
|
|
19
|
+
"prompts": { "name": "content" }
|
|
20
|
+
}
|
|
21
|
+
"""
|
|
22
|
+
skills_root = root / "Toolkit" / "skills"
|
|
23
|
+
skills_root.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
|
|
25
|
+
# 1. Write Skills
|
|
26
|
+
for res in resources:
|
|
27
|
+
if "skills" in res:
|
|
28
|
+
for name, content in res["skills"].items():
|
|
29
|
+
target_dir = skills_root / name
|
|
30
|
+
target_dir.mkdir(exist_ok=True)
|
|
31
|
+
target_file = target_dir / "SKILL.md"
|
|
32
|
+
# Idempotency: Overwrite if content is different? Or just always overwrite?
|
|
33
|
+
# User asked for "scaffold", implies creation.
|
|
34
|
+
# Let's overwrite to ensure extensive "Repair" capability.
|
|
35
|
+
target_file.write_text(content, encoding="utf-8")
|
|
36
|
+
console.print(f"[dim] - Scaffolding skill:[/dim] {name}")
|
|
37
|
+
|
|
38
|
+
# 2. Update Agent Docs
|
|
39
|
+
update_agent_docs(root, resources)
|
|
40
|
+
|
|
41
|
+
def update_agent_docs(root: Path, resources: List[Dict[str, Any]]):
|
|
42
|
+
"""
|
|
43
|
+
Inject prompts into AGENTS.md, GEMINI.md, CLAUDE.md.
|
|
44
|
+
"""
|
|
45
|
+
target_files = ["AGENTS.md", "GEMINI.md", "CLAUDE.md"]
|
|
46
|
+
|
|
47
|
+
# Aggregate Prompts
|
|
48
|
+
aggregated_prompt = "\n\n".join([
|
|
49
|
+
res["prompts"][name]
|
|
50
|
+
for res in resources
|
|
51
|
+
if "prompts" in res
|
|
52
|
+
for name in res["prompts"]
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
injection_content = f"""
|
|
56
|
+
## Monoco Toolkit
|
|
57
|
+
|
|
58
|
+
The following tools and skills are available in this environment.
|
|
59
|
+
|
|
60
|
+
{aggregated_prompt}
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
for filename in target_files:
|
|
64
|
+
_inject_section(root / filename, injection_content)
|
|
65
|
+
|
|
66
|
+
def _inject_section(file_path: Path, content: str):
|
|
67
|
+
if not file_path.exists():
|
|
68
|
+
# Create if not exists? User said "Edit AGENTS.md...", implies existence.
|
|
69
|
+
# But if we init in a fresh repo, maybe we should create them?
|
|
70
|
+
# Let's create if missing.
|
|
71
|
+
file_path.write_text(f"# Project Guidelines\n{content}", encoding="utf-8")
|
|
72
|
+
console.print(f"[green]✔[/green] Created {file_path.name}")
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
original_content = file_path.read_text(encoding="utf-8")
|
|
76
|
+
|
|
77
|
+
# Regex to find existing section
|
|
78
|
+
# Matches ## Monoco Toolkit ... until next ## or End of String
|
|
79
|
+
pattern = r"(## Monoco Toolkit.*?)(\n## |\Z)"
|
|
80
|
+
|
|
81
|
+
# Check if section exists
|
|
82
|
+
if re.search(pattern, original_content, re.DOTALL):
|
|
83
|
+
# Replace
|
|
84
|
+
new_content = re.sub(pattern, f"{content.strip()}\n\n\\2", original_content, flags=re.DOTALL)
|
|
85
|
+
if new_content != original_content:
|
|
86
|
+
file_path.write_text(new_content, encoding="utf-8")
|
|
87
|
+
console.print(f"[green]✔[/green] Updated {file_path.name}")
|
|
88
|
+
else:
|
|
89
|
+
console.print(f"[dim] - {file_path.name} is up to date.[/dim]")
|
|
90
|
+
else:
|
|
91
|
+
# Append
|
|
92
|
+
with open(file_path, "a", encoding="utf-8") as f:
|
|
93
|
+
if not original_content.endswith("\n"):
|
|
94
|
+
f.write("\n")
|
|
95
|
+
f.write(content)
|
|
96
|
+
console.print(f"[green]✔[/green] Appended to {file_path.name}")
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
|
|
5
|
+
from monoco.core.config import get_config
|
|
6
|
+
from . import core
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(help="Spike & Repo Management.")
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
@app.command("init")
|
|
12
|
+
def init():
|
|
13
|
+
"""Initialize the Spike environment (gitignore setup)."""
|
|
14
|
+
config = get_config()
|
|
15
|
+
root_dir = Path(config.paths.root)
|
|
16
|
+
spikes_dir_name = config.paths.spikes
|
|
17
|
+
|
|
18
|
+
core.ensure_gitignore(root_dir, spikes_dir_name)
|
|
19
|
+
|
|
20
|
+
# Create the directory
|
|
21
|
+
(root_dir / spikes_dir_name).mkdir(exist_ok=True)
|
|
22
|
+
|
|
23
|
+
console.print(f"[green]✔[/green] Initialized Spike environment. Added '{spikes_dir_name}/' to .gitignore.")
|
|
24
|
+
|
|
25
|
+
@app.command("add")
|
|
26
|
+
def add_repo(
|
|
27
|
+
url: str = typer.Argument(..., help="Git Repository URL"),
|
|
28
|
+
):
|
|
29
|
+
"""Add a new research repository."""
|
|
30
|
+
config = get_config()
|
|
31
|
+
root_dir = Path(config.paths.root)
|
|
32
|
+
|
|
33
|
+
# Infer name from URL
|
|
34
|
+
# e.g., https://github.com/foo/bar.git -> bar
|
|
35
|
+
# e.g., git@github.com:foo/bar.git -> bar
|
|
36
|
+
name = url.split("/")[-1]
|
|
37
|
+
if name.endswith(".git"):
|
|
38
|
+
name = name[:-4]
|
|
39
|
+
|
|
40
|
+
core.update_config_repos(root_dir, name, url)
|
|
41
|
+
console.print(f"[green]✔[/green] Added repo [bold]{name}[/bold] ({url}) to configuration.")
|
|
42
|
+
console.print("Run [bold]monoco spike sync[/bold] to download content.")
|
|
43
|
+
|
|
44
|
+
@app.command("remove")
|
|
45
|
+
def remove_repo(
|
|
46
|
+
name: str = typer.Argument(..., help="Repository Name"),
|
|
47
|
+
force: bool = typer.Option(False, "--force", "-f", help="Force delete physical directory without asking"),
|
|
48
|
+
):
|
|
49
|
+
"""Remove a repository from configuration."""
|
|
50
|
+
config = get_config()
|
|
51
|
+
root_dir = Path(config.paths.root)
|
|
52
|
+
spikes_dir = root_dir / config.paths.spikes
|
|
53
|
+
|
|
54
|
+
if name not in config.project.spike_repos:
|
|
55
|
+
console.print(f"[yellow]![/yellow] Repo [bold]{name}[/bold] not found in configuration.")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
# Remove from config
|
|
59
|
+
core.update_config_repos(root_dir, name, "", remove=True)
|
|
60
|
+
console.print(f"[green]✔[/green] Removed [bold]{name}[/bold] from configuration.")
|
|
61
|
+
|
|
62
|
+
target_path = spikes_dir / name
|
|
63
|
+
if target_path.exists():
|
|
64
|
+
if force or typer.confirm(f"Do you want to delete the directory {target_path}?", default=False):
|
|
65
|
+
core.remove_repo_dir(spikes_dir, name)
|
|
66
|
+
console.print(f"[gray]✔[/gray] Deleted directory {target_path}.")
|
|
67
|
+
else:
|
|
68
|
+
console.print(f"[gray]ℹ[/gray] Directory {target_path} kept.")
|
|
69
|
+
|
|
70
|
+
@app.command("sync")
|
|
71
|
+
def sync_repos():
|
|
72
|
+
"""Sync (Clone/Pull) all configured repositories."""
|
|
73
|
+
# Force reload config to get latest updates
|
|
74
|
+
config = get_config()
|
|
75
|
+
# Note: get_config is a singleton, so for 'add' then 'sync' in same process,
|
|
76
|
+
# we rely on 'add' writing to disk and us reading from memory?
|
|
77
|
+
# Actually, if we run standard CLI "monoco spike add" then "monoco spike sync",
|
|
78
|
+
# they are separate processes, so config loads fresh.
|
|
79
|
+
|
|
80
|
+
root_dir = Path(config.paths.root)
|
|
81
|
+
spikes_dir = root_dir / config.paths.spikes
|
|
82
|
+
spikes_dir.mkdir(exist_ok=True)
|
|
83
|
+
|
|
84
|
+
repos = config.project.spike_repos
|
|
85
|
+
|
|
86
|
+
if not repos:
|
|
87
|
+
console.print("[yellow]No repositories configured.[/yellow] Use 'monoco spike add <url>' first.")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
console.print(f"Syncing {len(repos)} repositories...")
|
|
91
|
+
|
|
92
|
+
for name, url in repos.items():
|
|
93
|
+
core.sync_repo(root_dir, spikes_dir, name, url)
|
|
94
|
+
|
|
95
|
+
console.print("[green]✔[/green] Sync complete.")
|
|
96
|
+
|
|
97
|
+
# Alias for list (showing configured repos) could be useful but not strictly asked for.
|
|
98
|
+
# Let's add a simple list command to see what we have.
|
|
99
|
+
@app.command("list")
|
|
100
|
+
def list_repos():
|
|
101
|
+
"""List configured repositories."""
|
|
102
|
+
config = get_config()
|
|
103
|
+
repos = config.project.spike_repos
|
|
104
|
+
|
|
105
|
+
if not repos:
|
|
106
|
+
console.print("[yellow]No repositories configured.[/yellow]")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
for name, url in repos.items():
|
|
110
|
+
console.print(f"- [bold]{name}[/bold]: {url}")
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import yaml
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Optional, List, Any
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from monoco.core.config import get_config
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
def run_git_command(cmd: List[str], cwd: Path) -> bool:
|
|
14
|
+
"""Run a git command in the specified directory."""
|
|
15
|
+
try:
|
|
16
|
+
subprocess.run(
|
|
17
|
+
cmd,
|
|
18
|
+
cwd=cwd,
|
|
19
|
+
check=True,
|
|
20
|
+
stdout=subprocess.PIPE,
|
|
21
|
+
stderr=subprocess.PIPE,
|
|
22
|
+
text=True
|
|
23
|
+
)
|
|
24
|
+
return True
|
|
25
|
+
except subprocess.CalledProcessError as e:
|
|
26
|
+
console.print(f"[red]Git Error:[/red] {' '.join(cmd)}\n{e.stderr}")
|
|
27
|
+
return False
|
|
28
|
+
except FileNotFoundError:
|
|
29
|
+
console.print("[red]Error:[/red] git command not found.")
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
def get_config_file_path(root: Path) -> Path:
|
|
33
|
+
"""Determine the config file to update."""
|
|
34
|
+
# Priority 1: .monoco/config.yaml
|
|
35
|
+
hidden = root / ".monoco" / "config.yaml"
|
|
36
|
+
if hidden.exists():
|
|
37
|
+
return hidden
|
|
38
|
+
|
|
39
|
+
# Priority 2: monoco.yaml
|
|
40
|
+
visible = root / "monoco.yaml"
|
|
41
|
+
if visible.exists():
|
|
42
|
+
return visible
|
|
43
|
+
|
|
44
|
+
# Default to .monoco/config.yaml for new files
|
|
45
|
+
hidden.parent.mkdir(exist_ok=True)
|
|
46
|
+
return hidden
|
|
47
|
+
|
|
48
|
+
def update_config_repos(root: Path, repo_name: str, repo_url: str, remove: bool = False):
|
|
49
|
+
"""Update the repos list in the config file."""
|
|
50
|
+
config_path = get_config_file_path(root)
|
|
51
|
+
|
|
52
|
+
data = {}
|
|
53
|
+
if config_path.exists():
|
|
54
|
+
try:
|
|
55
|
+
with open(config_path, "r") as f:
|
|
56
|
+
data = yaml.safe_load(f) or {}
|
|
57
|
+
except Exception:
|
|
58
|
+
data = {}
|
|
59
|
+
|
|
60
|
+
# Ensure structure exists
|
|
61
|
+
if "project" not in data:
|
|
62
|
+
data["project"] = {}
|
|
63
|
+
if "spike_repos" not in data["project"]:
|
|
64
|
+
data["project"]["spike_repos"] = {}
|
|
65
|
+
|
|
66
|
+
if remove:
|
|
67
|
+
if repo_name in data["project"]["spike_repos"]:
|
|
68
|
+
del data["project"]["spike_repos"][repo_name]
|
|
69
|
+
else:
|
|
70
|
+
data["project"]["spike_repos"][repo_name] = repo_url
|
|
71
|
+
|
|
72
|
+
with open(config_path, "w") as f:
|
|
73
|
+
yaml.dump(data, f, sort_keys=False, default_flow_style=False)
|
|
74
|
+
|
|
75
|
+
def ensure_gitignore(root: Path, target_dir_name: str):
|
|
76
|
+
"""Ensure the target directory is in .gitignore."""
|
|
77
|
+
gitignore = root / ".gitignore"
|
|
78
|
+
if not gitignore.exists():
|
|
79
|
+
gitignore.write_text(f"{target_dir_name}/\n")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
content = gitignore.read_text()
|
|
83
|
+
if f"{target_dir_name}/" not in content and f"{target_dir_name}" not in content:
|
|
84
|
+
# Avoid redundant newlines if file ends with one
|
|
85
|
+
prefix = "\n" if content and not content.endswith("\n") else ""
|
|
86
|
+
with open(gitignore, "a") as f:
|
|
87
|
+
f.write(f"{prefix}{target_dir_name}/\n")
|
|
88
|
+
|
|
89
|
+
def sync_repo(root: Path, spikes_dir: Path, name: str, url: str):
|
|
90
|
+
"""Clone or Pull a repo."""
|
|
91
|
+
target_path = spikes_dir / name
|
|
92
|
+
|
|
93
|
+
if target_path.exists() and (target_path / ".git").exists():
|
|
94
|
+
console.print(f"Updating [bold]{name}[/bold]...")
|
|
95
|
+
run_git_command(["git", "pull"], cwd=target_path)
|
|
96
|
+
else:
|
|
97
|
+
# If dir exists but not a git repo, warn or error?
|
|
98
|
+
# For safety, if non-empty and not git, skip or error.
|
|
99
|
+
if target_path.exists() and any(target_path.iterdir()):
|
|
100
|
+
console.print(f"[yellow]Skipping {name}:[/yellow] Directory exists and is not empty, but not a git repo.")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
console.print(f"Cloning [bold]{name}[/bold]...")
|
|
104
|
+
target_path.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
run_git_command(["git", "clone", url, "."], cwd=target_path)
|
|
106
|
+
|
|
107
|
+
def remove_repo_dir(spikes_dir: Path, name: str):
|
|
108
|
+
"""Physically remove the repo directory."""
|
|
109
|
+
target_path = spikes_dir / name
|
|
110
|
+
|
|
111
|
+
if target_path.exists():
|
|
112
|
+
shutil.rmtree(target_path)
|
|
113
|
+
|
|
114
|
+
SKILL_CONTENT = """---
|
|
115
|
+
name: git-repo-spike
|
|
116
|
+
description: Manage external Git repositories as References in `.reference/`.
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
# Git Repo Spike (Reference Management)
|
|
120
|
+
|
|
121
|
+
This skill normalizes how we introduce external code repositories.
|
|
122
|
+
|
|
123
|
+
## Core Principles
|
|
124
|
+
1. **Read-Only**: Code in `.reference/` is for reference only.
|
|
125
|
+
2. **Isolation**: All external repos sit within `.reference/`.
|
|
126
|
+
3. **VCS Hygiene**: `.reference/` is gitignored. We track the intent to clone, not the files.
|
|
127
|
+
|
|
128
|
+
## Workflow
|
|
129
|
+
1. **Add**: `monoco spike add <url>`
|
|
130
|
+
2. **Sync**: `monoco spike sync` (Clones/Pulls all repos)
|
|
131
|
+
3. **Remove**: `monoco spike remove <name>`
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
PROMPT_CONTENT = """### Spike (Research)
|
|
135
|
+
Manage external reference repositories.
|
|
136
|
+
- **Add Repo**: `monoco spike add <url>` (Available in `.reference/<name>` for reading)
|
|
137
|
+
- **Sync**: `monoco spike sync` (Run to download content)
|
|
138
|
+
- **Constraint**: Never edit files in `.reference/`. Treat them as read-only external knowledge."""
|
|
139
|
+
|
|
140
|
+
def init(root: Path, spikes_dir_name: str):
|
|
141
|
+
"""Initialize Spike environment."""
|
|
142
|
+
ensure_gitignore(root, spikes_dir_name)
|
|
143
|
+
(root / spikes_dir_name).mkdir(exist_ok=True)
|
|
144
|
+
|
|
145
|
+
def get_resources() -> Dict[str, Any]:
|
|
146
|
+
return {
|
|
147
|
+
"skills": {
|
|
148
|
+
"git-repo-spike": SKILL_CONTENT
|
|
149
|
+
},
|
|
150
|
+
"prompts": {
|
|
151
|
+
"spike": PROMPT_CONTENT
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
monoco/main.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import typer
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from monoco.core.output import print_output
|
|
5
|
+
|
|
6
|
+
app = typer.Typer(
|
|
7
|
+
name="monoco",
|
|
8
|
+
help="Monoco Agent Native Toolkit",
|
|
9
|
+
add_completion=False,
|
|
10
|
+
no_args_is_help=True
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def version_callback(value: bool):
|
|
15
|
+
if value:
|
|
16
|
+
import importlib.metadata
|
|
17
|
+
try:
|
|
18
|
+
version = importlib.metadata.version("monoco-toolkit")
|
|
19
|
+
except importlib.metadata.PackageNotFoundError:
|
|
20
|
+
version = "unknown"
|
|
21
|
+
print(f"Monoco Toolkit v{version}")
|
|
22
|
+
raise typer.Exit()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.callback()
|
|
26
|
+
def main(
|
|
27
|
+
ctx: typer.Context,
|
|
28
|
+
version: Optional[bool] = typer.Option(
|
|
29
|
+
None, "--version", "-v", help="Show version and exit", callback=version_callback, is_eager=True
|
|
30
|
+
),
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Monoco Toolkit - The sensory and motor system for Monoco Agents.
|
|
34
|
+
"""
|
|
35
|
+
# Capture command execution
|
|
36
|
+
from monoco.core.telemetry import capture_event
|
|
37
|
+
if ctx.invoked_subcommand:
|
|
38
|
+
capture_event("cli_command_executed", {"command": ctx.invoked_subcommand})
|
|
39
|
+
|
|
40
|
+
from monoco.core.setup import init_cli
|
|
41
|
+
app.command(name="init")(init_cli)
|
|
42
|
+
|
|
43
|
+
@app.command()
|
|
44
|
+
def info():
|
|
45
|
+
"""
|
|
46
|
+
Show toolkit information and current mode.
|
|
47
|
+
"""
|
|
48
|
+
from pydantic import BaseModel
|
|
49
|
+
from monoco.core.config import get_config
|
|
50
|
+
|
|
51
|
+
settings = get_config()
|
|
52
|
+
|
|
53
|
+
class Status(BaseModel):
|
|
54
|
+
version: str
|
|
55
|
+
mode: str
|
|
56
|
+
root: str
|
|
57
|
+
project: str
|
|
58
|
+
|
|
59
|
+
mode = "Agent (JSON)" if os.getenv("AGENT_FLAG") == "true" else "Human (Rich)"
|
|
60
|
+
|
|
61
|
+
import importlib.metadata
|
|
62
|
+
try:
|
|
63
|
+
version = importlib.metadata.version("monoco-toolkit")
|
|
64
|
+
except importlib.metadata.PackageNotFoundError:
|
|
65
|
+
version = "unknown"
|
|
66
|
+
|
|
67
|
+
status = Status(
|
|
68
|
+
version=version,
|
|
69
|
+
mode=mode,
|
|
70
|
+
root=os.getcwd(),
|
|
71
|
+
project=f"{settings.project.name} ({settings.project.key})"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
print_output(status, title="Monoco Toolkit Status")
|
|
75
|
+
|
|
76
|
+
if mode == "Human (Rich)":
|
|
77
|
+
print_output(settings, title="Current Configuration")
|
|
78
|
+
|
|
79
|
+
# Register Feature Modules
|
|
80
|
+
# Register Feature Modules
|
|
81
|
+
from monoco.features.issue import commands as issue_cmd
|
|
82
|
+
from monoco.features.spike import commands as spike_cmd
|
|
83
|
+
from monoco.features.i18n import commands as i18n_cmd
|
|
84
|
+
from monoco.features.config import commands as config_cmd
|
|
85
|
+
|
|
86
|
+
app.add_typer(issue_cmd.app, name="issue", help="Manage development issues")
|
|
87
|
+
app.add_typer(spike_cmd.app, name="spike", help="Manage research spikes")
|
|
88
|
+
app.add_typer(i18n_cmd.app, name="i18n", help="Manage documentation i18n")
|
|
89
|
+
app.add_typer(config_cmd.app, name="config", help="Manage configuration")
|
|
90
|
+
|
|
91
|
+
from monoco.daemon.commands import serve
|
|
92
|
+
app.command(name="serve")(serve)
|
|
93
|
+
|
|
94
|
+
@app.command()
|
|
95
|
+
def pty(
|
|
96
|
+
host: str = "127.0.0.1",
|
|
97
|
+
port: int = 3124,
|
|
98
|
+
cwd: Optional[str] = None
|
|
99
|
+
):
|
|
100
|
+
"""
|
|
101
|
+
Start the Monoco PTY Daemon (WebSocket).
|
|
102
|
+
"""
|
|
103
|
+
from monoco.features.pty.server import run_pty_server
|
|
104
|
+
from pathlib import Path
|
|
105
|
+
|
|
106
|
+
path = Path(cwd) if cwd else None
|
|
107
|
+
run_pty_server(host, port, path)
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
app()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: monoco-toolkit
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: Agent Native Toolkit for Monoco - Task Management & Kanban for AI Agents
|
|
5
|
+
Project-URL: Homepage, https://monoco.io
|
|
6
|
+
Project-URL: Repository, https://github.com/IndenScale/Monoco
|
|
7
|
+
Project-URL: Documentation, https://monoco.io/docs
|
|
8
|
+
Project-URL: Issues, https://github.com/IndenScale/Monoco/issues
|
|
9
|
+
Author-email: Monoco Team <dev@monoco.io>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent-native,ai-agents,cli,kanban,monoco,task-management,workflow
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Office/Business :: Groupware
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: fastapi>=0.100.0
|
|
25
|
+
Requires-Dist: httpx>=0.28.1
|
|
26
|
+
Requires-Dist: prompt-toolkit>=3.0.0
|
|
27
|
+
Requires-Dist: pydantic>=2.0.0
|
|
28
|
+
Requires-Dist: pyyaml>=6.0
|
|
29
|
+
Requires-Dist: rich>=13.0.0
|
|
30
|
+
Requires-Dist: sse-starlette>=1.6.0
|
|
31
|
+
Requires-Dist: typer[all]>=0.9.0
|
|
32
|
+
Requires-Dist: uvicorn[standard]>=0.20.0
|
|
33
|
+
Requires-Dist: watchdog>=6.0.0
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# Monoco: Harnessing AI Agents
|
|
37
|
+
|
|
38
|
+
> **The control interface between raw AI velocity and human information bandwidth.**
|
|
39
|
+
|
|
40
|
+
Production in the LLM era is exploding along a vertical curve. A single AI agent can work 24/7, generating massive amounts of intermediate data that far exceeds the biological information bandwidth of a human supervisor. When one agent becomes a hundred, the bottleneck is no longer "intelligence"—it is "command and control."
|
|
41
|
+
|
|
42
|
+
**Monoco is the Cockpit.**
|
|
43
|
+
|
|
44
|
+
It doesn't just "run" agents; it "encapsulates" them. It provides a deterministic barrier between the chaotic, raw execution power of LLMs and the rigorous, finite decision bandwidth of human engineers. It ensures that every agentic action eventually collapses into the outcome you intended.
|
|
45
|
+
|
|
46
|
+
## Workflow: Plan - Execute - Review - Archive
|
|
47
|
+
|
|
48
|
+
Monoco channels agent execution into a clear cycle:
|
|
49
|
+
|
|
50
|
+
1. **Plan**: Decompose complex missions through **Project → Epic → Feature** hierarchies into executable atomic units.
|
|
51
|
+
2. **Execute**: Agents work autonomously based on acceptance criteria defined in Issues, with all intermediate states persisted as structured files.
|
|
52
|
+
3. **Review**: Humans monitor progress through the Kanban dashboard, intervening only at critical decision points.
|
|
53
|
+
4. **Archive**: Completed tasks automatically transition to archived states, forming a traceable project history.
|
|
54
|
+
|
|
55
|
+
## The Control Matrix
|
|
56
|
+
|
|
57
|
+
- **Task Anchors (Issues)**: Define missions via structured files, setting clear boundaries and acceptance criteria for agents.
|
|
58
|
+
- **Deterministic Interface (CLI)**: Acts as a sensory extension for LLMs, providing them with structured perception of project state and eliminating hallucinated guesses.
|
|
59
|
+
- **Mission Dashboard (Kanban)**: A high-fidelity visual console that allows humans to audit tasks and transition states with minimal cognitive load.
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
### 1. Install the Control Suite
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install monoco-toolkit
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 2. Initialize the Workflow
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
monoco init
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. Take Control
|
|
76
|
+
|
|
77
|
+
Start the backend control hub:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
monoco serve
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Then, launch the visual mission dashboard from anywhere:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx @monoco-io/kanban
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Visit `http://localhost:3123` (or the URL displayed in your terminal) to enter your cockpit.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
_"Cars are made to drive, not to fix."_
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
monoco/main.py,sha256=_16blpwjqnSsmahwj9lJ6Cgm_6OVYw9BsdORepY3zX8,3058
|
|
2
|
+
monoco/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
monoco/core/config.py,sha256=oiWB4bYkLGNhV9J3nGHdYjnVIux0gi8u9oYEpcvanpA,4768
|
|
4
|
+
monoco/core/git.py,sha256=Qy5VjCKe0Y1y0rjqYULxT_viS7S4phTljf8hkd9DA8Q,6424
|
|
5
|
+
monoco/core/output.py,sha256=CK8efvj0Q-pWrcJMdXwbuCyfsykWZ_pen9YWuDsivXQ,3192
|
|
6
|
+
monoco/core/setup.py,sha256=qbMh6LV5wvXNYwquzxpmf3JZAoNDcwLClnNZ2QXPajs,8406
|
|
7
|
+
monoco/core/telemetry.py,sha256=DZQGOhvpe0XL34RCDaTZEUhmkD4abBTZumZJQlALzpY,2923
|
|
8
|
+
monoco/core/workspace.py,sha256=pFMC2culomxOF6Q1XqpEHCB8WjEEPBxKObUsOVpIG-E,1306
|
|
9
|
+
monoco/daemon/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
monoco/daemon/app.py,sha256=RdNVjaB1JJUJ3_qCfKzBWyTRoNZV10KsBxoMHG--To8,12819
|
|
11
|
+
monoco/daemon/commands.py,sha256=dN4D8ca0vPjj0WyjCSs8senneta1afm_bnNYv_kmGlU,1125
|
|
12
|
+
monoco/daemon/models.py,sha256=DqhR8_t0Oz1Tptqkw9KGZMgvPyL8mvb01aFdbmKqi4M,812
|
|
13
|
+
monoco/daemon/reproduce_stats.py,sha256=Q_zAj0Yj8l-77QDdtsLz1kWr68HeO7f1T6xC6VP43Vo,1261
|
|
14
|
+
monoco/daemon/services.py,sha256=ExSPd2J4QtzrHPG_sYjNMDtA3p_3xmQXo_gfL-2oIUY,9151
|
|
15
|
+
monoco/daemon/stats.py,sha256=r-L0k6CdxkAkwLZV3V-jW7PldB9S3uNklQGLCEKA3Sc,4563
|
|
16
|
+
monoco/features/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
monoco/features/config/commands.py,sha256=BySoGkIYia6RTsouDREAyjLV14n-_sZnInzPDlPdYCo,2139
|
|
18
|
+
monoco/features/i18n/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
monoco/features/i18n/commands.py,sha256=M_T8Ddw54p42wsZIowFjBq72mXKLsppzBRcHUByfhls,4816
|
|
20
|
+
monoco/features/i18n/core.py,sha256=recjfNhJA2xdqFUMIivZ2omQZEzbgXJ1QvhjmvEDj24,5957
|
|
21
|
+
monoco/features/issue/commands.py,sha256=w89KS19pJfMRzbIMIECQatalvPe4Q5rT8vSPfUnRQik,29448
|
|
22
|
+
monoco/features/issue/core.py,sha256=iX-p7fTQUThKcAYOzlbBHldqG5naJV012cBvIxYIeFA,43778
|
|
23
|
+
monoco/features/issue/linter.py,sha256=ZMNpp_0ehbzBMYROfPtCr4O4JL8mhdO9L0F3EAi96lE,7657
|
|
24
|
+
monoco/features/issue/models.py,sha256=sepGxdgvRFRSc4tyvcUB9oXFBkDyS9_g0bGZEaE8Eq8,4970
|
|
25
|
+
monoco/features/pty/core.py,sha256=eM1EvHQrgExSnatO15pyfhh1VZoz0BBTfNYdXqG8HZ8,5711
|
|
26
|
+
monoco/features/pty/router.py,sha256=7h80EPpOuE7hX5ifkxkzffcLZGecd5X8OmNvOji5ToI,5078
|
|
27
|
+
monoco/features/pty/server.py,sha256=kw2csMZ_R4_Xx6ta2dbznWtgNZLfrWOAkMp8NjlZYBc,1920
|
|
28
|
+
monoco/features/skills/__init__.py,sha256=L8YNGPWyyFWq5WqNossfeB0AKHJF_omrn1VzJBrRFcM,23
|
|
29
|
+
monoco/features/skills/core.py,sha256=mpd0Cq-k2MvHRTPq9saFvZgYXUBGJ9pnK5lUmzUfZbY,3418
|
|
30
|
+
monoco/features/spike/commands.py,sha256=BpwYYIpihmezJACOxgQojsCM7RwXqL0_EB6wCl--Mm8,3947
|
|
31
|
+
monoco/features/spike/core.py,sha256=FSlO3Lhz49_nyFp68TpxBdkFKksShz5S33Qxh74ZFpY,5150
|
|
32
|
+
monoco_toolkit-0.1.5.dist-info/METADATA,sha256=1j6huqrvTPN4rMnw_rdB8RweTuvf85jrlCrgP2HpKTY,3743
|
|
33
|
+
monoco_toolkit-0.1.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
34
|
+
monoco_toolkit-0.1.5.dist-info/entry_points.txt,sha256=iYj7FWYBdtClU15-Du1skqD0s6SFSIhJvxJ29VWp8ng,43
|
|
35
|
+
monoco_toolkit-0.1.5.dist-info/licenses/LICENSE,sha256=ACAGGjV6aod4eIlVUTx1q9PZbnZGN5bBwkSs9RHj83s,1071
|
|
36
|
+
monoco_toolkit-0.1.5.dist-info/RECORD,,
|