monoco-toolkit 0.2.5__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/core/agent/adapters.py +24 -1
- monoco/core/config.py +63 -1
- monoco/core/integrations.py +8 -0
- monoco/core/lsp.py +7 -0
- monoco/core/output.py +8 -1
- monoco/core/setup.py +8 -0
- monoco/features/agent/commands.py +73 -2
- 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/issue/commands.py +133 -35
- 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 +181 -65
- monoco/features/spike/core.py +5 -22
- monoco/main.py +0 -15
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/METADATA +1 -1
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/RECORD +41 -22
- 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.5.dist-info → monoco_toolkit-0.2.6.dist-info}/WHEEL +0 -0
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/entry_points.txt +0 -0
- {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/licenses/LICENSE +0 -0
monoco/features/pty/core.py
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import asyncio
|
|
3
|
-
import os
|
|
4
|
-
import pty
|
|
5
|
-
import select
|
|
6
|
-
import signal
|
|
7
|
-
import struct
|
|
8
|
-
import fcntl
|
|
9
|
-
import termios
|
|
10
|
-
import logging
|
|
11
|
-
from typing import Dict, Optional, Tuple, Any
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger("monoco.pty")
|
|
14
|
-
|
|
15
|
-
class PTYSession:
|
|
16
|
-
"""
|
|
17
|
-
Manages a single PTY session connected to a subprocess (shell).
|
|
18
|
-
"""
|
|
19
|
-
def __init__(self, session_id: str, cmd: list[str], env: Optional[Dict[str, str]] = None, cwd: Optional[str] = None):
|
|
20
|
-
self.session_id = session_id
|
|
21
|
-
self.cmd = cmd
|
|
22
|
-
self.env = env or os.environ.copy()
|
|
23
|
-
self.cwd = cwd or os.getcwd()
|
|
24
|
-
|
|
25
|
-
self.fd: Optional[int] = None
|
|
26
|
-
self.pid: Optional[int] = None
|
|
27
|
-
self.proc = None # subprocess.Popen object
|
|
28
|
-
self.running = False
|
|
29
|
-
self.loop = asyncio.get_running_loop()
|
|
30
|
-
|
|
31
|
-
def start(self, cols: int = 80, rows: int = 24):
|
|
32
|
-
"""
|
|
33
|
-
Spawn a subprocess connected to a new PTY using subprocess.Popen.
|
|
34
|
-
This provides better safety in threaded/asyncio environments than pty.fork().
|
|
35
|
-
"""
|
|
36
|
-
import subprocess
|
|
37
|
-
|
|
38
|
-
# 1. Open PTY pair
|
|
39
|
-
master_fd, slave_fd = pty.openpty()
|
|
40
|
-
|
|
41
|
-
# 2. Set initial size
|
|
42
|
-
self._set_winsize(master_fd, rows, cols)
|
|
43
|
-
|
|
44
|
-
try:
|
|
45
|
-
# 3. Spawn process
|
|
46
|
-
# start_new_session=True executes setsid()
|
|
47
|
-
self.proc = subprocess.Popen(
|
|
48
|
-
self.cmd,
|
|
49
|
-
stdin=slave_fd,
|
|
50
|
-
stdout=slave_fd,
|
|
51
|
-
stderr=slave_fd,
|
|
52
|
-
cwd=self.cwd,
|
|
53
|
-
env=self.env,
|
|
54
|
-
start_new_session=True,
|
|
55
|
-
close_fds=True # Important to close other FDs in child
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
self.pid = self.proc.pid
|
|
59
|
-
self.fd = master_fd
|
|
60
|
-
self.running = True
|
|
61
|
-
|
|
62
|
-
# 4. Close slave fd in parent (child has it open now)
|
|
63
|
-
os.close(slave_fd)
|
|
64
|
-
|
|
65
|
-
logger.info(f"Started session {self.session_id} (PID: {self.pid})")
|
|
66
|
-
|
|
67
|
-
except Exception as e:
|
|
68
|
-
logger.error(f"Failed to spawn process: {e}")
|
|
69
|
-
# Ensure we clean up fds if spawn fails
|
|
70
|
-
try:
|
|
71
|
-
os.close(master_fd)
|
|
72
|
-
except: pass
|
|
73
|
-
try:
|
|
74
|
-
os.close(slave_fd)
|
|
75
|
-
except: pass
|
|
76
|
-
raise e
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def resize(self, cols: int, rows: int):
|
|
80
|
-
"""
|
|
81
|
-
Resize the PTY.
|
|
82
|
-
"""
|
|
83
|
-
if self.fd and self.running:
|
|
84
|
-
self._set_winsize(self.fd, rows, cols)
|
|
85
|
-
|
|
86
|
-
def write(self, data: bytes):
|
|
87
|
-
"""
|
|
88
|
-
Write input data (from websocket) to the PTY master fd.
|
|
89
|
-
"""
|
|
90
|
-
if self.fd and self.running:
|
|
91
|
-
os.write(self.fd, data)
|
|
92
|
-
|
|
93
|
-
async def read(self) -> bytes:
|
|
94
|
-
"""
|
|
95
|
-
Read output data from PTY master fd (to forward to websocket).
|
|
96
|
-
"""
|
|
97
|
-
if not self.fd or not self.running:
|
|
98
|
-
return b""
|
|
99
|
-
|
|
100
|
-
try:
|
|
101
|
-
# Run in executor to avoid blocking the event loop
|
|
102
|
-
# pty read is blocking
|
|
103
|
-
return await self.loop.run_in_executor(None, self._read_blocking)
|
|
104
|
-
except OSError:
|
|
105
|
-
return b""
|
|
106
|
-
|
|
107
|
-
def _read_blocking(self) -> bytes:
|
|
108
|
-
try:
|
|
109
|
-
return os.read(self.fd, 1024)
|
|
110
|
-
except OSError:
|
|
111
|
-
return b""
|
|
112
|
-
|
|
113
|
-
def terminate(self):
|
|
114
|
-
"""
|
|
115
|
-
Terminate the process and close the PTY.
|
|
116
|
-
"""
|
|
117
|
-
self.running = False
|
|
118
|
-
|
|
119
|
-
# Use Popen object if available
|
|
120
|
-
if self.proc:
|
|
121
|
-
try:
|
|
122
|
-
self.proc.terminate()
|
|
123
|
-
try:
|
|
124
|
-
self.proc.wait(timeout=1.0)
|
|
125
|
-
except:
|
|
126
|
-
# Force kill if not terminated
|
|
127
|
-
self.proc.kill()
|
|
128
|
-
self.proc.wait()
|
|
129
|
-
except Exception as e:
|
|
130
|
-
logger.error(f"Error terminating process: {e}")
|
|
131
|
-
self.proc = None
|
|
132
|
-
self.pid = None
|
|
133
|
-
elif self.pid:
|
|
134
|
-
# Fallback for legacy or if Popen obj lost
|
|
135
|
-
try:
|
|
136
|
-
os.kill(self.pid, signal.SIGTERM)
|
|
137
|
-
os.waitpid(self.pid, 0) # Reap zombie
|
|
138
|
-
except OSError:
|
|
139
|
-
pass
|
|
140
|
-
self.pid = None
|
|
141
|
-
|
|
142
|
-
if self.fd:
|
|
143
|
-
try:
|
|
144
|
-
os.close(self.fd)
|
|
145
|
-
except OSError:
|
|
146
|
-
pass
|
|
147
|
-
self.fd = None
|
|
148
|
-
logger.info(f"Terminated session {self.session_id}")
|
|
149
|
-
|
|
150
|
-
def _set_winsize(self, fd: int, row: int, col: int, xpix: int = 0, ypix: int = 0):
|
|
151
|
-
winsize = struct.pack("HHHH", row, col, xpix, ypix)
|
|
152
|
-
fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
class PTYManager:
|
|
156
|
-
"""
|
|
157
|
-
Singleton to manage multiple PTY sessions.
|
|
158
|
-
"""
|
|
159
|
-
def __init__(self):
|
|
160
|
-
self.sessions: Dict[str, PTYSession] = {}
|
|
161
|
-
|
|
162
|
-
def create_session(self, session_id: str, cwd: str, cmd: list[str] = ["/bin/zsh"], env: Dict = None) -> PTYSession:
|
|
163
|
-
if session_id in self.sessions:
|
|
164
|
-
# In a real app, we might want to attach to existing?
|
|
165
|
-
# For now, kill and recreate (or error)
|
|
166
|
-
self.close_session(session_id)
|
|
167
|
-
|
|
168
|
-
session = PTYSession(session_id, cmd, env, cwd)
|
|
169
|
-
self.sessions[session_id] = session
|
|
170
|
-
return session
|
|
171
|
-
|
|
172
|
-
def get_session(self, session_id: str) -> Optional[PTYSession]:
|
|
173
|
-
return self.sessions.get(session_id)
|
|
174
|
-
|
|
175
|
-
def close_session(self, session_id: str):
|
|
176
|
-
if session_id in self.sessions:
|
|
177
|
-
self.sessions[session_id].terminate()
|
|
178
|
-
del self.sessions[session_id]
|
|
179
|
-
|
|
180
|
-
def close_all_sessions(self):
|
|
181
|
-
"""
|
|
182
|
-
Terminate all active PTY sessions.
|
|
183
|
-
"""
|
|
184
|
-
for session_id in list(self.sessions.keys()):
|
|
185
|
-
self.close_session(session_id)
|
monoco/features/pty/router.py
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
|
|
3
|
-
from pydantic import BaseModel
|
|
4
|
-
from typing import Optional, Dict
|
|
5
|
-
import json
|
|
6
|
-
import asyncio
|
|
7
|
-
import logging
|
|
8
|
-
import os
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from monoco.features.pty.core import PTYManager
|
|
11
|
-
from monoco.core.config import get_config
|
|
12
|
-
|
|
13
|
-
# We will use dependency injection or a global singleton for now
|
|
14
|
-
# Ideally attached to app state
|
|
15
|
-
pty_manager = PTYManager()
|
|
16
|
-
|
|
17
|
-
router = APIRouter(prefix="/api/v1/pty", tags=["pty"])
|
|
18
|
-
|
|
19
|
-
logger = logging.getLogger("monoco.pty")
|
|
20
|
-
|
|
21
|
-
@router.websocket("/ws/{session_id}")
|
|
22
|
-
async def websocket_pty_endpoint(
|
|
23
|
-
websocket: WebSocket,
|
|
24
|
-
session_id: str,
|
|
25
|
-
cwd: Optional[str] = Query(None),
|
|
26
|
-
cols: int = Query(80),
|
|
27
|
-
rows: int = Query(24),
|
|
28
|
-
env: Optional[str] = Query(None) # JSON-encoded env vars
|
|
29
|
-
):
|
|
30
|
-
await websocket.accept()
|
|
31
|
-
|
|
32
|
-
# Determine working directory
|
|
33
|
-
# 1. Provide explicit CWD in query
|
|
34
|
-
# 2. Or fallback to ProjectRoot from env (if integrated)
|
|
35
|
-
# 3. Or fallback to process CWD
|
|
36
|
-
|
|
37
|
-
# Since monoco pty runs as a separate service, we expect CWD to be passed
|
|
38
|
-
# or we default to where monoco pty was started
|
|
39
|
-
working_dir = cwd if cwd else os.getcwd()
|
|
40
|
-
|
|
41
|
-
# Prepare environment
|
|
42
|
-
env_vars = os.environ.copy()
|
|
43
|
-
env_vars["TERM"] = "xterm-256color"
|
|
44
|
-
env_vars["COLORTERM"] = "truecolor"
|
|
45
|
-
if "SHELL" not in env_vars:
|
|
46
|
-
env_vars["SHELL"] = "/bin/zsh"
|
|
47
|
-
if "HOME" not in env_vars:
|
|
48
|
-
import pathlib
|
|
49
|
-
env_vars["HOME"] = str(pathlib.Path.home())
|
|
50
|
-
|
|
51
|
-
# Filter out Trae/Gemini specific variables to avoid shell integration conflicts
|
|
52
|
-
# This prevents the shell from trying to write to IDE-specific logs which causes EPERM
|
|
53
|
-
keys_to_remove = [k for k in env_vars.keys() if k.startswith("TRAE_") or k.startswith("GEMINI_") or k == "AI_AGENT"]
|
|
54
|
-
for k in keys_to_remove:
|
|
55
|
-
del env_vars[k]
|
|
56
|
-
|
|
57
|
-
if env:
|
|
58
|
-
try:
|
|
59
|
-
custom_env = json.loads(env)
|
|
60
|
-
env_vars.update(custom_env)
|
|
61
|
-
except:
|
|
62
|
-
logger.warning("Failed to parse custom env vars")
|
|
63
|
-
|
|
64
|
-
# Start Session
|
|
65
|
-
try:
|
|
66
|
-
session = pty_manager.create_session(
|
|
67
|
-
session_id=session_id,
|
|
68
|
-
cwd=working_dir,
|
|
69
|
-
cmd=["/bin/zsh", "-l"], # Use login shell to ensure full user environment
|
|
70
|
-
env=env_vars
|
|
71
|
-
)
|
|
72
|
-
session.start(cols, rows)
|
|
73
|
-
except Exception as e:
|
|
74
|
-
logger.error(f"Failed to start session: {e}")
|
|
75
|
-
await websocket.close(code=1011)
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
# Pipe Loop
|
|
79
|
-
reader_task = None
|
|
80
|
-
try:
|
|
81
|
-
# Task to read from PTY and send to WebSocket
|
|
82
|
-
async def pty_reader():
|
|
83
|
-
while session.running:
|
|
84
|
-
data = await session.read()
|
|
85
|
-
if not data:
|
|
86
|
-
break
|
|
87
|
-
# xterm.js expects string or binary. We send string/bytes.
|
|
88
|
-
# Usually text is fine, but binary is safer for control codes.
|
|
89
|
-
await websocket.send_bytes(data)
|
|
90
|
-
|
|
91
|
-
# If PTY exits, close WS
|
|
92
|
-
await websocket.close()
|
|
93
|
-
|
|
94
|
-
reader_task = asyncio.create_task(pty_reader())
|
|
95
|
-
|
|
96
|
-
# Main loop: Read from WebSocket and write to PTY
|
|
97
|
-
try:
|
|
98
|
-
while True:
|
|
99
|
-
# Receive message from Client (xterm.js)
|
|
100
|
-
# Message can be simple input string, or a JSON command (resize)
|
|
101
|
-
message = await websocket.receive()
|
|
102
|
-
|
|
103
|
-
if message["type"] == "websocket.disconnect":
|
|
104
|
-
raise WebSocketDisconnect(code=message.get("code", 1000))
|
|
105
|
-
|
|
106
|
-
if "text" in message:
|
|
107
|
-
payload = message["text"]
|
|
108
|
-
|
|
109
|
-
# Check if it's a control message (Hack: usually client sends raw input)
|
|
110
|
-
# We can enforce a protocol: binary for Input, text JSON for Control.
|
|
111
|
-
try:
|
|
112
|
-
# Try parsing as JSON control message
|
|
113
|
-
cmd = json.loads(payload)
|
|
114
|
-
if cmd.get("type") == "resize":
|
|
115
|
-
session.resize(cmd["cols"], cmd["rows"])
|
|
116
|
-
continue
|
|
117
|
-
except:
|
|
118
|
-
pass # Not JSON, treat as raw input
|
|
119
|
-
|
|
120
|
-
session.write(payload.encode())
|
|
121
|
-
|
|
122
|
-
elif "bytes" in message:
|
|
123
|
-
session.write(message["bytes"])
|
|
124
|
-
except RuntimeError:
|
|
125
|
-
# Handle "Cannot call 'receive' once a disconnect message has been received"
|
|
126
|
-
# This happens if Starlette/FastAPI already processed the disconnect internally
|
|
127
|
-
# but we called receive() again.
|
|
128
|
-
logger.info(f"Runtime disconnect for session {session_id}")
|
|
129
|
-
|
|
130
|
-
except WebSocketDisconnect:
|
|
131
|
-
logger.info(f"Client disconnected for session {session_id}")
|
|
132
|
-
except Exception as e:
|
|
133
|
-
logger.error(f"WebSocket error: {e}")
|
|
134
|
-
finally:
|
|
135
|
-
# Cleanup
|
|
136
|
-
pty_manager.close_session(session_id)
|
|
137
|
-
if reader_task and not reader_task.done():
|
|
138
|
-
reader_task.cancel()
|
monoco/features/pty/server.py
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|