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.
Files changed (44) hide show
  1. monoco/core/agent/adapters.py +24 -1
  2. monoco/core/config.py +63 -1
  3. monoco/core/integrations.py +8 -0
  4. monoco/core/lsp.py +7 -0
  5. monoco/core/output.py +8 -1
  6. monoco/core/setup.py +8 -0
  7. monoco/features/agent/commands.py +73 -2
  8. monoco/features/agent/core.py +48 -0
  9. monoco/features/agent/resources/en/critique.prompty +16 -0
  10. monoco/features/agent/resources/en/develop.prompty +16 -0
  11. monoco/features/agent/resources/en/investigate.prompty +16 -0
  12. monoco/features/agent/resources/en/refine.prompty +14 -0
  13. monoco/features/agent/resources/en/verify.prompty +16 -0
  14. monoco/features/agent/resources/zh/critique.prompty +18 -0
  15. monoco/features/agent/resources/zh/develop.prompty +18 -0
  16. monoco/features/agent/resources/zh/investigate.prompty +18 -0
  17. monoco/features/agent/resources/zh/refine.prompty +16 -0
  18. monoco/features/agent/resources/zh/verify.prompty +18 -0
  19. monoco/features/issue/commands.py +133 -35
  20. monoco/features/issue/core.py +142 -119
  21. monoco/features/issue/domain/__init__.py +0 -0
  22. monoco/features/issue/domain/lifecycle.py +126 -0
  23. monoco/features/issue/domain/models.py +170 -0
  24. monoco/features/issue/domain/parser.py +223 -0
  25. monoco/features/issue/domain/workspace.py +104 -0
  26. monoco/features/issue/engine/__init__.py +22 -0
  27. monoco/features/issue/engine/config.py +189 -0
  28. monoco/features/issue/engine/machine.py +185 -0
  29. monoco/features/issue/engine/models.py +18 -0
  30. monoco/features/issue/linter.py +32 -11
  31. monoco/features/issue/lsp/__init__.py +3 -0
  32. monoco/features/issue/lsp/definition.py +72 -0
  33. monoco/features/issue/models.py +8 -8
  34. monoco/features/issue/validator.py +181 -65
  35. monoco/features/spike/core.py +5 -22
  36. monoco/main.py +0 -15
  37. {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/METADATA +1 -1
  38. {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/RECORD +41 -22
  39. monoco/features/pty/core.py +0 -185
  40. monoco/features/pty/router.py +0 -138
  41. monoco/features/pty/server.py +0 -56
  42. {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/WHEEL +0 -0
  43. {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/entry_points.txt +0 -0
  44. {monoco_toolkit-0.2.5.dist-info → monoco_toolkit-0.2.6.dist-info}/licenses/LICENSE +0 -0
@@ -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)
@@ -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()
@@ -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()