monkeyscode 1.0.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.
- monkeyscode/__init__.py +178 -0
- monkeyscode/_http.py +83 -0
- monkeyscode/admin.py +141 -0
- monkeyscode/agent.py +772 -0
- monkeyscode/ci.py +288 -0
- monkeyscode/cli_process.py +730 -0
- monkeyscode/daemon.py +201 -0
- monkeyscode/events.py +323 -0
- monkeyscode/export.py +319 -0
- monkeyscode/hooks.py +190 -0
- monkeyscode/mcp.py +283 -0
- monkeyscode/orchestrator.py +170 -0
- monkeyscode/otel.py +181 -0
- monkeyscode/py.typed +1 -0
- monkeyscode/runs.py +138 -0
- monkeyscode/sandbox.py +125 -0
- monkeyscode/session.py +227 -0
- monkeyscode/subagent.py +195 -0
- monkeyscode/telemetry.py +204 -0
- monkeyscode/tools.py +133 -0
- monkeyscode/watcher.py +137 -0
- monkeyscode-1.0.0.dist-info/METADATA +152 -0
- monkeyscode-1.0.0.dist-info/RECORD +25 -0
- monkeyscode-1.0.0.dist-info/WHEEL +4 -0
- monkeyscode-1.0.0.dist-info/licenses/LICENSE +21 -0
monkeyscode/ci.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MonkeysCode SDK — CI/CD Helpers.
|
|
3
|
+
|
|
4
|
+
Utilities for running MonkeysCode in CI pipelines:
|
|
5
|
+
- Headless runs with structured output
|
|
6
|
+
- SARIF report generation for GitHub Code Scanning
|
|
7
|
+
- GitHub Actions annotations
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from monkeyscode.agent import MonkeysCode
|
|
18
|
+
from monkeyscode.events import AgentEvent, AgentResult, CompleteEvent, ErrorEvent, TextEvent
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class CiFinding:
|
|
23
|
+
"""A single finding/issue reported by the agent."""
|
|
24
|
+
|
|
25
|
+
severity: str # 'error', 'warning', 'note', 'high', 'medium', 'low'
|
|
26
|
+
message: str
|
|
27
|
+
file: str | None = None
|
|
28
|
+
line: int | None = None
|
|
29
|
+
end_line: int | None = None
|
|
30
|
+
column: int | None = None
|
|
31
|
+
rule_id: str | None = None
|
|
32
|
+
category: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class CiResult:
|
|
37
|
+
"""Result from a CI run."""
|
|
38
|
+
|
|
39
|
+
success: bool
|
|
40
|
+
exit_code: int
|
|
41
|
+
result: AgentResult
|
|
42
|
+
findings: list[CiFinding] = field(default_factory=list)
|
|
43
|
+
events: list[AgentEvent] = field(default_factory=list)
|
|
44
|
+
duration_ms: int = 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Exit codes
|
|
48
|
+
CI_EXIT_SUCCESS = 0
|
|
49
|
+
CI_EXIT_ERROR = 1
|
|
50
|
+
CI_EXIT_FINDINGS = 10
|
|
51
|
+
CI_EXIT_HIGH_SEVERITY = 11
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def ci_run(
|
|
55
|
+
*,
|
|
56
|
+
prompt: str,
|
|
57
|
+
api_key: str | None = None,
|
|
58
|
+
model: str = "auto",
|
|
59
|
+
working_directory: str | None = None,
|
|
60
|
+
diff: str | None = None,
|
|
61
|
+
timeout: int = 600_000,
|
|
62
|
+
on_event: Any = None,
|
|
63
|
+
) -> CiResult:
|
|
64
|
+
"""
|
|
65
|
+
Run the agent in headless CI mode.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
prompt: The instruction for the agent.
|
|
69
|
+
api_key: API key.
|
|
70
|
+
model: Model to use.
|
|
71
|
+
working_directory: Working directory.
|
|
72
|
+
diff: Git diff content to provide as context.
|
|
73
|
+
timeout: Timeout in ms.
|
|
74
|
+
on_event: Optional callback for streaming events.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
CiResult with findings, exit code, and events.
|
|
78
|
+
"""
|
|
79
|
+
start_time = time.monotonic()
|
|
80
|
+
events: list[AgentEvent] = []
|
|
81
|
+
|
|
82
|
+
agent = MonkeysCode(
|
|
83
|
+
api_key=api_key,
|
|
84
|
+
model=model,
|
|
85
|
+
working_directory=working_directory,
|
|
86
|
+
timeout=timeout,
|
|
87
|
+
permissions_mode="autoApprove",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Build prompt with diff context
|
|
91
|
+
full_prompt = prompt
|
|
92
|
+
if diff:
|
|
93
|
+
full_prompt = f"{prompt}\n\nHere is the diff to review:\n```diff\n{diff}\n```"
|
|
94
|
+
|
|
95
|
+
async for event in agent.stream(full_prompt):
|
|
96
|
+
events.append(event)
|
|
97
|
+
if on_event:
|
|
98
|
+
on_event(event)
|
|
99
|
+
|
|
100
|
+
# Build result
|
|
101
|
+
result = AgentResult(success=False, summary="Agent failed", model=model)
|
|
102
|
+
for event in events:
|
|
103
|
+
if isinstance(event, CompleteEvent):
|
|
104
|
+
result = AgentResult(
|
|
105
|
+
success=True,
|
|
106
|
+
summary=event.summary,
|
|
107
|
+
files_changed=event.files_changed,
|
|
108
|
+
tokens=event.tokens,
|
|
109
|
+
cost=event.cost,
|
|
110
|
+
duration_ms=event.duration_ms,
|
|
111
|
+
model=model,
|
|
112
|
+
)
|
|
113
|
+
break
|
|
114
|
+
if isinstance(event, ErrorEvent):
|
|
115
|
+
result = AgentResult(
|
|
116
|
+
success=False,
|
|
117
|
+
summary=event.message,
|
|
118
|
+
exit_code=1,
|
|
119
|
+
model=model,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Extract findings
|
|
123
|
+
findings = _extract_findings(events)
|
|
124
|
+
|
|
125
|
+
# Determine exit code
|
|
126
|
+
exit_code = CI_EXIT_SUCCESS
|
|
127
|
+
if not result.success:
|
|
128
|
+
exit_code = CI_EXIT_ERROR
|
|
129
|
+
elif any(f.severity in ("high", "error") for f in findings):
|
|
130
|
+
exit_code = CI_EXIT_HIGH_SEVERITY
|
|
131
|
+
elif findings:
|
|
132
|
+
exit_code = CI_EXIT_FINDINGS
|
|
133
|
+
|
|
134
|
+
duration_ms = int((time.monotonic() - start_time) * 1000)
|
|
135
|
+
|
|
136
|
+
return CiResult(
|
|
137
|
+
success=result.success,
|
|
138
|
+
exit_code=exit_code,
|
|
139
|
+
result=result,
|
|
140
|
+
findings=findings,
|
|
141
|
+
events=events,
|
|
142
|
+
duration_ms=duration_ms,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def to_sarif(result: CiResult) -> dict[str, Any]:
|
|
147
|
+
"""Convert a CI result to SARIF 2.1.0 format."""
|
|
148
|
+
rules: dict[str, dict[str, Any]] = {}
|
|
149
|
+
sarif_results: list[dict[str, Any]] = []
|
|
150
|
+
|
|
151
|
+
for finding in result.findings:
|
|
152
|
+
rule_id = finding.rule_id or finding.category or "monkeyscode-review"
|
|
153
|
+
|
|
154
|
+
if rule_id not in rules:
|
|
155
|
+
rules[rule_id] = {
|
|
156
|
+
"id": rule_id,
|
|
157
|
+
"shortDescription": {"text": finding.category or rule_id},
|
|
158
|
+
"defaultConfiguration": {"level": _map_severity(finding.severity)},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
sarif_result: dict[str, Any] = {
|
|
162
|
+
"ruleId": rule_id,
|
|
163
|
+
"message": {"text": finding.message},
|
|
164
|
+
"level": _map_severity(finding.severity),
|
|
165
|
+
"locations": [],
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if finding.file:
|
|
169
|
+
location: dict[str, Any] = {
|
|
170
|
+
"physicalLocation": {
|
|
171
|
+
"artifactLocation": {"uri": finding.file},
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
if finding.line:
|
|
175
|
+
location["physicalLocation"]["region"] = {
|
|
176
|
+
"startLine": finding.line,
|
|
177
|
+
}
|
|
178
|
+
if finding.end_line:
|
|
179
|
+
location["physicalLocation"]["region"]["endLine"] = finding.end_line
|
|
180
|
+
sarif_result["locations"].append(location)
|
|
181
|
+
|
|
182
|
+
sarif_results.append(sarif_result)
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
186
|
+
"version": "2.1.0",
|
|
187
|
+
"runs": [
|
|
188
|
+
{
|
|
189
|
+
"tool": {
|
|
190
|
+
"driver": {
|
|
191
|
+
"name": "MonkeysCode",
|
|
192
|
+
"version": "2.0.0",
|
|
193
|
+
"informationUri": "https://monkeyscode.com",
|
|
194
|
+
"rules": list(rules.values()),
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
"results": sarif_results,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def print_github_annotations(findings: list[CiFinding]) -> None:
|
|
204
|
+
"""Print GitHub Actions annotations for inline PR display."""
|
|
205
|
+
for finding in findings:
|
|
206
|
+
level = "error" if finding.severity in ("high", "error") else "warning"
|
|
207
|
+
file_part = f"file={finding.file}," if finding.file else ""
|
|
208
|
+
line_part = f"line={finding.line}," if finding.line else ""
|
|
209
|
+
title = finding.rule_id or "MonkeysCode Review"
|
|
210
|
+
print(f"::{level} {file_part}{line_part}title={title}::{finding.message}")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def print_ci_summary(result: CiResult) -> None:
|
|
214
|
+
"""Print a human-readable CI summary."""
|
|
215
|
+
findings = result.findings
|
|
216
|
+
errors = sum(1 for f in findings if f.severity in ("high", "error"))
|
|
217
|
+
warnings = sum(1 for f in findings if f.severity in ("medium", "warning"))
|
|
218
|
+
notes = sum(1 for f in findings if f.severity in ("low", "note"))
|
|
219
|
+
|
|
220
|
+
print()
|
|
221
|
+
print("MonkeysCode CI Review")
|
|
222
|
+
print(f" Duration: {result.duration_ms / 1000:.1f}s")
|
|
223
|
+
print(f" Findings: {len(findings)} total")
|
|
224
|
+
if errors:
|
|
225
|
+
print(f" Errors: {errors}")
|
|
226
|
+
if warnings:
|
|
227
|
+
print(f" Warnings: {warnings}")
|
|
228
|
+
if notes:
|
|
229
|
+
print(f" Notes: {notes}")
|
|
230
|
+
print(f" Exit code: {result.exit_code}")
|
|
231
|
+
print()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ── Private ──────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _extract_findings(events: list[AgentEvent]) -> list[CiFinding]:
|
|
238
|
+
"""Extract findings from agent events."""
|
|
239
|
+
findings: list[CiFinding] = []
|
|
240
|
+
|
|
241
|
+
for event in events:
|
|
242
|
+
if not isinstance(event, TextEvent):
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
for line in event.content.split("\n"):
|
|
246
|
+
# BUG/ERROR/VULNERABILITY patterns
|
|
247
|
+
bug_match = re.match(
|
|
248
|
+
r"(?:BUG|ERROR|CRITICAL|VULNERABILITY):\s*(.+)", line, re.IGNORECASE,
|
|
249
|
+
)
|
|
250
|
+
if bug_match:
|
|
251
|
+
findings.append(
|
|
252
|
+
CiFinding(severity="high", message=bug_match.group(1).strip(), category="bug"),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# WARNING patterns
|
|
256
|
+
warn_match = re.match(r"(?:WARNING|WARN|ISSUE):\s*(.+)", line, re.IGNORECASE)
|
|
257
|
+
if warn_match:
|
|
258
|
+
findings.append(
|
|
259
|
+
CiFinding(
|
|
260
|
+
severity="warning", message=warn_match.group(1).strip(), category="warning",
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# File:line patterns
|
|
265
|
+
file_match = re.match(r"([^\s:]+\.[a-z]+):(\d+):\s*(.+)", line, re.IGNORECASE)
|
|
266
|
+
if file_match:
|
|
267
|
+
findings.append(
|
|
268
|
+
CiFinding(
|
|
269
|
+
severity="warning",
|
|
270
|
+
file=file_match.group(1),
|
|
271
|
+
line=int(file_match.group(2)),
|
|
272
|
+
message=file_match.group(3).strip(),
|
|
273
|
+
),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return findings
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _map_severity(severity: str) -> str:
|
|
280
|
+
"""Map severity to SARIF level."""
|
|
281
|
+
return {
|
|
282
|
+
"error": "error",
|
|
283
|
+
"high": "error",
|
|
284
|
+
"warning": "warning",
|
|
285
|
+
"medium": "warning",
|
|
286
|
+
"note": "note",
|
|
287
|
+
"low": "note",
|
|
288
|
+
}.get(severity, "warning")
|