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/telemetry.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MonkeysCode SDK — Telemetry (Opt-in).
|
|
3
|
+
|
|
4
|
+
Anonymous usage telemetry for understanding SDK adoption.
|
|
5
|
+
Disabled by default — must be explicitly enabled.
|
|
6
|
+
|
|
7
|
+
No PII is ever collected. Only:
|
|
8
|
+
- SDK version
|
|
9
|
+
- Model used
|
|
10
|
+
- Event counts (not content)
|
|
11
|
+
- Duration and token totals
|
|
12
|
+
- Error codes (not messages)
|
|
13
|
+
|
|
14
|
+
Usage::
|
|
15
|
+
|
|
16
|
+
from monkeyscode.telemetry import TelemetryCollector, with_telemetry
|
|
17
|
+
|
|
18
|
+
agent = with_telemetry(agent, TelemetryOptions(enabled=True))
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
import httpx
|
|
29
|
+
|
|
30
|
+
SDK_VERSION = "1.0.0"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ── Types ────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class TelemetryOptions:
|
|
38
|
+
"""Telemetry configuration."""
|
|
39
|
+
|
|
40
|
+
endpoint: str = "https://telemetry.monkeyscode.com/v1/events"
|
|
41
|
+
enabled: bool = False
|
|
42
|
+
metadata: dict[str, str] = field(default_factory=dict)
|
|
43
|
+
flush_interval_ms: int = 30_000
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class TelemetryEvent:
|
|
48
|
+
"""A telemetry event (anonymous)."""
|
|
49
|
+
|
|
50
|
+
type: str # "run_start" | "run_complete" | "run_error"
|
|
51
|
+
timestamp: str = ""
|
|
52
|
+
sdk_version: str = SDK_VERSION
|
|
53
|
+
model: str = ""
|
|
54
|
+
event_counts: dict[str, int] = field(default_factory=dict)
|
|
55
|
+
tokens: dict[str, int] = field(default_factory=dict)
|
|
56
|
+
duration_ms: float = 0
|
|
57
|
+
error_code: str = ""
|
|
58
|
+
metadata: dict[str, str] = field(default_factory=dict)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ── Collector ────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TelemetryCollector:
|
|
65
|
+
"""
|
|
66
|
+
Collects and sends anonymous telemetry events.
|
|
67
|
+
|
|
68
|
+
Events are buffered and flushed periodically or on close.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, options: TelemetryOptions | None = None) -> None:
|
|
72
|
+
self._options = options or TelemetryOptions()
|
|
73
|
+
self._buffer: list[TelemetryEvent] = []
|
|
74
|
+
self._task: asyncio.Task[None] | None = None
|
|
75
|
+
|
|
76
|
+
if self._options.enabled:
|
|
77
|
+
try:
|
|
78
|
+
loop = asyncio.get_event_loop()
|
|
79
|
+
if loop.is_running():
|
|
80
|
+
self._task = loop.create_task(self._flush_loop())
|
|
81
|
+
except RuntimeError:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def enabled(self) -> bool:
|
|
86
|
+
return self._options.enabled
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def buffer_size(self) -> int:
|
|
90
|
+
return len(self._buffer)
|
|
91
|
+
|
|
92
|
+
def record_run_start(self, model: str) -> None:
|
|
93
|
+
"""Record a run start event."""
|
|
94
|
+
if not self._options.enabled:
|
|
95
|
+
return
|
|
96
|
+
self._buffer.append(TelemetryEvent(
|
|
97
|
+
type="run_start",
|
|
98
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
99
|
+
model=model,
|
|
100
|
+
metadata=self._options.metadata,
|
|
101
|
+
))
|
|
102
|
+
|
|
103
|
+
def record_run_complete(
|
|
104
|
+
self,
|
|
105
|
+
model: str,
|
|
106
|
+
*,
|
|
107
|
+
tokens: dict[str, int] | None = None,
|
|
108
|
+
duration_ms: float = 0,
|
|
109
|
+
event_counts: dict[str, int] | None = None,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Record a run completion event."""
|
|
112
|
+
if not self._options.enabled:
|
|
113
|
+
return
|
|
114
|
+
self._buffer.append(TelemetryEvent(
|
|
115
|
+
type="run_complete",
|
|
116
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
117
|
+
model=model,
|
|
118
|
+
tokens=tokens or {},
|
|
119
|
+
duration_ms=duration_ms,
|
|
120
|
+
event_counts=event_counts or {},
|
|
121
|
+
metadata=self._options.metadata,
|
|
122
|
+
))
|
|
123
|
+
|
|
124
|
+
def record_error(self, model: str, error_code: str) -> None:
|
|
125
|
+
"""Record a run error (code only, no PII)."""
|
|
126
|
+
if not self._options.enabled:
|
|
127
|
+
return
|
|
128
|
+
self._buffer.append(TelemetryEvent(
|
|
129
|
+
type="run_error",
|
|
130
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
131
|
+
model=model,
|
|
132
|
+
error_code=error_code,
|
|
133
|
+
metadata=self._options.metadata,
|
|
134
|
+
))
|
|
135
|
+
|
|
136
|
+
async def flush(self) -> None:
|
|
137
|
+
"""Flush buffered events to the telemetry endpoint."""
|
|
138
|
+
if not self._options.enabled or not self._buffer:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
events = self._buffer[:]
|
|
142
|
+
self._buffer.clear()
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
async with httpx.AsyncClient() as client:
|
|
146
|
+
payload = {
|
|
147
|
+
"events": [
|
|
148
|
+
{
|
|
149
|
+
"type": e.type,
|
|
150
|
+
"timestamp": e.timestamp,
|
|
151
|
+
"sdkVersion": e.sdk_version,
|
|
152
|
+
"model": e.model,
|
|
153
|
+
"eventCounts": e.event_counts,
|
|
154
|
+
"tokens": e.tokens,
|
|
155
|
+
"durationMs": e.duration_ms,
|
|
156
|
+
"errorCode": e.error_code,
|
|
157
|
+
"metadata": e.metadata,
|
|
158
|
+
}
|
|
159
|
+
for e in events
|
|
160
|
+
],
|
|
161
|
+
}
|
|
162
|
+
await client.post(
|
|
163
|
+
self._options.endpoint,
|
|
164
|
+
json=payload,
|
|
165
|
+
timeout=5.0,
|
|
166
|
+
)
|
|
167
|
+
except Exception:
|
|
168
|
+
pass # Telemetry should never break the app
|
|
169
|
+
|
|
170
|
+
async def close(self) -> None:
|
|
171
|
+
"""Stop collecting and flush remaining events."""
|
|
172
|
+
if self._task is not None:
|
|
173
|
+
self._task.cancel()
|
|
174
|
+
self._task = None
|
|
175
|
+
await self.flush()
|
|
176
|
+
|
|
177
|
+
async def _flush_loop(self) -> None:
|
|
178
|
+
"""Background loop to flush events periodically."""
|
|
179
|
+
interval = self._options.flush_interval_ms / 1000
|
|
180
|
+
while True:
|
|
181
|
+
await asyncio.sleep(interval)
|
|
182
|
+
await self.flush()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ── Agent Integration ────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def with_telemetry(agent: Any, options: TelemetryOptions | None = None) -> Any:
|
|
189
|
+
"""
|
|
190
|
+
Wrap a MonkeysCode agent with telemetry collection.
|
|
191
|
+
|
|
192
|
+
Non-invasive — the original agent is returned unchanged,
|
|
193
|
+
but event listeners are attached to collect anonymous usage data.
|
|
194
|
+
"""
|
|
195
|
+
opts = options or TelemetryOptions()
|
|
196
|
+
if not opts.enabled:
|
|
197
|
+
return agent
|
|
198
|
+
|
|
199
|
+
collector = TelemetryCollector(opts)
|
|
200
|
+
|
|
201
|
+
agent.on("start", lambda e: collector.record_run_start(getattr(e, "model", "unknown")))
|
|
202
|
+
agent.on("error", lambda e: collector.record_error("unknown", getattr(e, "code", "UNKNOWN")))
|
|
203
|
+
|
|
204
|
+
return agent
|
monkeyscode/tools.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MonkeysCode SDK — Custom Tool Definitions.
|
|
3
|
+
|
|
4
|
+
Define tools that the agent can call during a run.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from monkeyscode import define_tool, MonkeysCode
|
|
9
|
+
|
|
10
|
+
@define_tool(
|
|
11
|
+
name="deploy",
|
|
12
|
+
description="Deploy the application to an environment",
|
|
13
|
+
parameters={"env": {"type": "string", "enum": ["staging", "prod"]}},
|
|
14
|
+
)
|
|
15
|
+
async def deploy(env: str) -> dict:
|
|
16
|
+
return {"deployed": True, "url": f"https://{env}.example.com"}
|
|
17
|
+
|
|
18
|
+
agent = MonkeysCode(api_key="mc_...")
|
|
19
|
+
agent.add_tool(deploy)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import functools
|
|
25
|
+
import inspect
|
|
26
|
+
from collections.abc import Callable
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ToolDefinition:
|
|
33
|
+
"""A custom tool that the agent can invoke."""
|
|
34
|
+
|
|
35
|
+
name: str
|
|
36
|
+
description: str
|
|
37
|
+
parameters: dict[str, Any] = field(default_factory=dict)
|
|
38
|
+
handler: Callable[..., Any] | None = None
|
|
39
|
+
|
|
40
|
+
def to_json_schema(self) -> dict[str, Any]:
|
|
41
|
+
"""Convert to the JSON Schema format expected by the model proxy."""
|
|
42
|
+
return {
|
|
43
|
+
"name": self.name,
|
|
44
|
+
"description": self.description,
|
|
45
|
+
"parameters": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"properties": self.parameters,
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async def execute(self, args: dict[str, Any]) -> Any:
|
|
52
|
+
"""Execute the tool with the given arguments."""
|
|
53
|
+
if self.handler is None:
|
|
54
|
+
raise RuntimeError(f"Tool '{self.name}' has no handler")
|
|
55
|
+
|
|
56
|
+
if inspect.iscoroutinefunction(self.handler):
|
|
57
|
+
return await self.handler(**args)
|
|
58
|
+
else:
|
|
59
|
+
return self.handler(**args)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def define_tool(
|
|
63
|
+
*,
|
|
64
|
+
name: str,
|
|
65
|
+
description: str,
|
|
66
|
+
parameters: dict[str, Any] | None = None,
|
|
67
|
+
) -> Callable[[Callable[..., Any]], ToolDefinition]:
|
|
68
|
+
"""
|
|
69
|
+
Decorator to define a custom tool.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
name: Unique tool name (e.g., 'deploy', 'send_email').
|
|
73
|
+
description: Human-readable description for the agent.
|
|
74
|
+
parameters: JSON Schema for the tool's parameters.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
A ToolDefinition wrapping the decorated function.
|
|
78
|
+
|
|
79
|
+
Example::
|
|
80
|
+
|
|
81
|
+
@define_tool(
|
|
82
|
+
name="deploy",
|
|
83
|
+
description="Deploy to staging",
|
|
84
|
+
parameters={"env": {"type": "string"}},
|
|
85
|
+
)
|
|
86
|
+
async def deploy(env: str) -> dict:
|
|
87
|
+
return {"deployed": True}
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def decorator(func: Callable[..., Any]) -> ToolDefinition:
|
|
91
|
+
tool = ToolDefinition(
|
|
92
|
+
name=name,
|
|
93
|
+
description=description,
|
|
94
|
+
parameters=parameters or _infer_parameters(func),
|
|
95
|
+
handler=func,
|
|
96
|
+
)
|
|
97
|
+
# Preserve function metadata
|
|
98
|
+
functools.update_wrapper(tool, func) # type: ignore[arg-type]
|
|
99
|
+
return tool
|
|
100
|
+
|
|
101
|
+
return decorator
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _infer_parameters(func: Callable[..., Any]) -> dict[str, Any]:
|
|
105
|
+
"""
|
|
106
|
+
Infer JSON Schema parameters from function type hints.
|
|
107
|
+
Falls back to 'string' for unannotated parameters.
|
|
108
|
+
"""
|
|
109
|
+
sig = inspect.signature(func)
|
|
110
|
+
properties: dict[str, Any] = {}
|
|
111
|
+
|
|
112
|
+
type_map = {
|
|
113
|
+
str: "string",
|
|
114
|
+
int: "integer",
|
|
115
|
+
float: "number",
|
|
116
|
+
bool: "boolean",
|
|
117
|
+
list: "array",
|
|
118
|
+
dict: "object",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for param_name, param in sig.parameters.items():
|
|
122
|
+
if param_name in ("self", "cls"):
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
annotation = param.annotation
|
|
126
|
+
json_type = "string" # default
|
|
127
|
+
|
|
128
|
+
if annotation != inspect.Parameter.empty:
|
|
129
|
+
json_type = type_map.get(annotation, "string")
|
|
130
|
+
|
|
131
|
+
properties[param_name] = {"type": json_type}
|
|
132
|
+
|
|
133
|
+
return properties
|
monkeyscode/watcher.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MonkeysCode SDK — File Watcher.
|
|
3
|
+
|
|
4
|
+
Debounced, recursive file watcher for watch mode.
|
|
5
|
+
Uses asyncio + os.walk polling (works cross-platform).
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
from monkeyscode.watcher import watch_files
|
|
10
|
+
|
|
11
|
+
async for batch in watch_files(directory="."):
|
|
12
|
+
print("Changed:", batch)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import os
|
|
19
|
+
from collections.abc import AsyncGenerator
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
|
|
22
|
+
# ── Types ────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
DEFAULT_IGNORES = [
|
|
25
|
+
"node_modules", ".git", "dist", "build", ".next", ".nuxt",
|
|
26
|
+
"target", "__pycache__", ".monkeyscode", ".DS_Store",
|
|
27
|
+
"package-lock.json", "pnpm-lock.yaml", "yarn.lock", "Cargo.lock",
|
|
28
|
+
".venv", "venv", ".mypy_cache", ".ruff_cache", ".pytest_cache",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class WatchOptions:
|
|
34
|
+
"""Options for the file watcher."""
|
|
35
|
+
|
|
36
|
+
directory: str = "."
|
|
37
|
+
ignore: list[str] = field(default_factory=list)
|
|
38
|
+
debounce_ms: int = 500
|
|
39
|
+
poll_interval_ms: int = 1000
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── Watcher ──────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def watch_files(
|
|
46
|
+
directory: str = ".",
|
|
47
|
+
*,
|
|
48
|
+
ignore: list[str] | None = None,
|
|
49
|
+
debounce_ms: int = 500,
|
|
50
|
+
poll_interval_ms: int = 1000,
|
|
51
|
+
) -> AsyncGenerator[list[str], None]:
|
|
52
|
+
"""
|
|
53
|
+
Watch a directory recursively and yield batches of changed file paths.
|
|
54
|
+
|
|
55
|
+
Uses polling for cross-platform compatibility.
|
|
56
|
+
Batches changes within the debounce window.
|
|
57
|
+
"""
|
|
58
|
+
all_ignores = set(DEFAULT_IGNORES + (ignore or []))
|
|
59
|
+
poll_interval = poll_interval_ms / 1000
|
|
60
|
+
debounce = debounce_ms / 1000
|
|
61
|
+
|
|
62
|
+
# Build initial snapshot
|
|
63
|
+
snapshot = _build_snapshot(directory, all_ignores)
|
|
64
|
+
|
|
65
|
+
while True:
|
|
66
|
+
await asyncio.sleep(poll_interval)
|
|
67
|
+
|
|
68
|
+
new_snapshot = _build_snapshot(directory, all_ignores)
|
|
69
|
+
changed = _diff_snapshots(snapshot, new_snapshot)
|
|
70
|
+
|
|
71
|
+
if changed:
|
|
72
|
+
# Debounce: wait and collect more changes
|
|
73
|
+
await asyncio.sleep(debounce)
|
|
74
|
+
final_snapshot = _build_snapshot(directory, all_ignores)
|
|
75
|
+
all_changed = _diff_snapshots(snapshot, final_snapshot)
|
|
76
|
+
snapshot = final_snapshot
|
|
77
|
+
|
|
78
|
+
if all_changed:
|
|
79
|
+
yield sorted(all_changed)
|
|
80
|
+
else:
|
|
81
|
+
snapshot = new_snapshot
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _build_snapshot(directory: str, ignores: set[str]) -> dict[str, float]:
|
|
85
|
+
"""Build a snapshot of file modification times."""
|
|
86
|
+
snapshot: dict[str, float] = {}
|
|
87
|
+
|
|
88
|
+
for root, dirs, files in os.walk(directory, topdown=True):
|
|
89
|
+
# Filter ignored directories in-place
|
|
90
|
+
dirs[:] = [d for d in dirs if not _should_ignore(d, ignores)]
|
|
91
|
+
|
|
92
|
+
for f in files:
|
|
93
|
+
if _should_ignore(f, ignores):
|
|
94
|
+
continue
|
|
95
|
+
path = os.path.join(root, f)
|
|
96
|
+
try:
|
|
97
|
+
snapshot[path] = os.path.getmtime(path)
|
|
98
|
+
except OSError:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
return snapshot
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _diff_snapshots(old: dict[str, float], new: dict[str, float]) -> list[str]:
|
|
105
|
+
"""Find changed files between two snapshots."""
|
|
106
|
+
changed: list[str] = []
|
|
107
|
+
|
|
108
|
+
# Modified or new files
|
|
109
|
+
for path, mtime in new.items():
|
|
110
|
+
old_mtime = old.get(path)
|
|
111
|
+
if old_mtime is None or mtime > old_mtime:
|
|
112
|
+
changed.append(path)
|
|
113
|
+
|
|
114
|
+
# Deleted files
|
|
115
|
+
for path in old:
|
|
116
|
+
if path not in new:
|
|
117
|
+
changed.append(path)
|
|
118
|
+
|
|
119
|
+
return changed
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _should_ignore(name: str, ignores: set[str]) -> bool:
|
|
123
|
+
"""Check if a file/directory should be ignored."""
|
|
124
|
+
if name.startswith("."):
|
|
125
|
+
# Allow .env files but ignore other dotfiles
|
|
126
|
+
if name not in (".env", ".env.local", ".env.development"):
|
|
127
|
+
return True
|
|
128
|
+
return name in ignores
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def format_change_batch(batch: list[str]) -> str:
|
|
132
|
+
"""Format a batch of changed files for display."""
|
|
133
|
+
if not batch:
|
|
134
|
+
return "No changes"
|
|
135
|
+
if len(batch) == 1:
|
|
136
|
+
return f"Changed: {batch[0]}"
|
|
137
|
+
return f"Changed {len(batch)} files:\n" + "\n".join(f" {f}" for f in batch[:20])
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: monkeyscode
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: MonkeysCode SDK — programmatic agent control for Python
|
|
5
|
+
Project-URL: Homepage, https://monkeyscode.com
|
|
6
|
+
Project-URL: Documentation, https://monkeyscode.com/docs/sdk/python
|
|
7
|
+
Project-URL: Repository, https://github.com/MonkeysCloud/monkeyscode
|
|
8
|
+
Project-URL: Issues, https://github.com/MonkeysCloud/monkeyscode/issues
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,automation,coding-agent,monkeyscode,sdk
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# MonkeysCode Python SDK
|
|
33
|
+
|
|
34
|
+
Programmatic agent control for Python. Run coding agents, stream events, define custom tools, and integrate with CI/CD pipelines.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install monkeyscode
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from monkeyscode import MonkeysCode
|
|
46
|
+
import asyncio
|
|
47
|
+
|
|
48
|
+
async def main():
|
|
49
|
+
agent = MonkeysCode(api_key="mc_...")
|
|
50
|
+
result = await agent.run("Fix all Python type errors")
|
|
51
|
+
print(f"✓ {result.summary}")
|
|
52
|
+
print(f" Files: {len(result.files_changed)}, Cost: ${result.cost:.4f}")
|
|
53
|
+
|
|
54
|
+
asyncio.run(main())
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Streaming
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
async for event in agent.stream("Refactor auth module"):
|
|
61
|
+
if event.type == "text":
|
|
62
|
+
print(event.content, end="", flush=True)
|
|
63
|
+
elif event.type == "tool_call":
|
|
64
|
+
print(f" 🔧 {event.tool}")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Custom Tools
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from monkeyscode import define_tool
|
|
71
|
+
|
|
72
|
+
@define_tool(
|
|
73
|
+
name="deploy",
|
|
74
|
+
description="Deploy the application",
|
|
75
|
+
parameters={"env": {"type": "string", "enum": ["staging", "prod"]}},
|
|
76
|
+
)
|
|
77
|
+
async def deploy(env: str) -> dict:
|
|
78
|
+
return {"deployed": True, "url": f"https://{env}.example.com"}
|
|
79
|
+
|
|
80
|
+
agent.add_tool(deploy)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Goal Mode
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
result = await agent.goal(
|
|
87
|
+
"All tests pass",
|
|
88
|
+
verify_command="pytest",
|
|
89
|
+
max_iterations=5,
|
|
90
|
+
)
|
|
91
|
+
print(f"Goal met: {result.goal_met} in {result.iterations} iterations")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Sessions
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from monkeyscode import Session
|
|
98
|
+
|
|
99
|
+
session = await Session.create(agent, workspace="/my/project")
|
|
100
|
+
await session.run("Add auth module")
|
|
101
|
+
await session.run("Now add tests for it") # Carries context
|
|
102
|
+
session.close()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Local agent (`mc` subprocess)
|
|
106
|
+
|
|
107
|
+
The client above calls the hosted API. To run the agent **on this machine**
|
|
108
|
+
with your settings, permission rules, hooks and MCP servers, drive an
|
|
109
|
+
installed `mc` (standard library only):
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from monkeyscode.cli_process import (
|
|
113
|
+
CliOptions, MonkeysCodeClient, PermissionAllow, PermissionDeny, ResultMessage, query,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
async for event in query("explain src/main.ts"):
|
|
117
|
+
if isinstance(event, ResultMessage):
|
|
118
|
+
print(event.result, event.cost_usd)
|
|
119
|
+
|
|
120
|
+
async def can_use_tool(tool, tool_input, ctx):
|
|
121
|
+
if str(tool_input.get("path", "")).startswith("docs/"):
|
|
122
|
+
return PermissionAllow()
|
|
123
|
+
return PermissionDeny(message="only docs/ may be written")
|
|
124
|
+
|
|
125
|
+
async with MonkeysCodeClient(CliOptions(can_use_tool=can_use_tool, max_cost_usd=1)) as client:
|
|
126
|
+
await client.send("write docs/intro.md")
|
|
127
|
+
result = await client.receive_result()
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`query_cli` and `MonkeysCodeClient` are also exported from `monkeyscode`.
|
|
131
|
+
Guide: [docs/cli/sdk.md](../../docs/cli/sdk.md); a runnable three-turn
|
|
132
|
+
example: [`examples/three_turn_approval.py`](examples/three_turn_approval.py).
|
|
133
|
+
|
|
134
|
+
## CI/CD
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from monkeyscode.ci import ci_run, to_sarif
|
|
138
|
+
import json
|
|
139
|
+
|
|
140
|
+
result = await ci_run(
|
|
141
|
+
prompt="Review this PR for security issues",
|
|
142
|
+
diff=open("pr.diff").read(),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
sarif = to_sarif(result)
|
|
146
|
+
with open("results.sarif", "w") as f:
|
|
147
|
+
json.dump(sarif, f, indent=2)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
monkeyscode/__init__.py,sha256=gWwGIwUJxcgZPXqnsl3z4JJ7JOMMQzhx4BD6CeR-fSY,4218
|
|
2
|
+
monkeyscode/_http.py,sha256=6fsUcxAHgufWNg-e-VAvFTYoqVCRQl4xXQdykwNdUIE,2237
|
|
3
|
+
monkeyscode/admin.py,sha256=7-1bLQjYMt9lf5rG6Y5k1-JQzbCpPFoLnb9TeV7NoDI,4717
|
|
4
|
+
monkeyscode/agent.py,sha256=S-zHhcos-J1mZeNCKOnHUwIltQHdSKjjkLIBKJpy3Q8,27938
|
|
5
|
+
monkeyscode/ci.py,sha256=dWz2t0At6H-rLL6X2l7dcLPIH6uMe2skWyTraeZQbqg,8912
|
|
6
|
+
monkeyscode/cli_process.py,sha256=ZjCtvogo5hsTT5uRuQzpYlzdS9SBSAuEsN1AUl2wfPA,25644
|
|
7
|
+
monkeyscode/daemon.py,sha256=MBjm-xgVq8jvlVf5QyR9IcyT7jB_u_iRGSVBr5tH4c4,6205
|
|
8
|
+
monkeyscode/events.py,sha256=JIOOq0V5sJEQ7BxXMHL39k_stiBndij7rRuaNJC3uR8,8467
|
|
9
|
+
monkeyscode/export.py,sha256=zw5kZGAEtVsMEINHQw9WJRoLcbSCrAkVY9_MOIfxE40,10234
|
|
10
|
+
monkeyscode/hooks.py,sha256=Hv3u_3RF-u1BdKsuGRdDUEYzPihBSj-S-RwvDnC97Rw,5938
|
|
11
|
+
monkeyscode/mcp.py,sha256=bpBsK2iFgfKE_nCqPNn8dgV0bWh_BJTKK9IpvtrqU_U,8431
|
|
12
|
+
monkeyscode/orchestrator.py,sha256=BWbpvLU36GQmCqc0Ynh_p95e1S-gA8DM4odY76_yDWQ,5164
|
|
13
|
+
monkeyscode/otel.py,sha256=1t5MvKur2Q_AAgU3Og1ToFdjhNoJyZIxi71Stsbm9_8,5696
|
|
14
|
+
monkeyscode/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
15
|
+
monkeyscode/runs.py,sha256=HFdbmFqKYEAmrkZKArcCtRtBRbKZsf2ciiJcyivKdNE,4386
|
|
16
|
+
monkeyscode/sandbox.py,sha256=-AkdU34MDckmIyU-cve-TF5czJK5HR301b9bR1bsc4Q,3247
|
|
17
|
+
monkeyscode/session.py,sha256=y0r-jDAzTRwczkhUgw-Vw_Avle40tY5wYlK4BAfqnAk,6673
|
|
18
|
+
monkeyscode/subagent.py,sha256=_hQ6fw0mlmgCB2da7vTqPXG41rDjPciejw9wwQcLL7A,6271
|
|
19
|
+
monkeyscode/telemetry.py,sha256=Tw99XDYb7hunHUlpDCfy31lbhoqmH12hco5zFYRtsYw,6510
|
|
20
|
+
monkeyscode/tools.py,sha256=wpEtCqA6QxfJAQ6RG39ydu3kfx-in5VXA6ff-erJ6i8,3625
|
|
21
|
+
monkeyscode/watcher.py,sha256=s0ComQf3-OgpdCo280IALY1LL9tMeD7dbL219oiVcYM,4231
|
|
22
|
+
monkeyscode-1.0.0.dist-info/METADATA,sha256=1E4CmiI3sYmU-ssRzxrkfFhjQipGe151R7pfiVL_B5U,4307
|
|
23
|
+
monkeyscode-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
24
|
+
monkeyscode-1.0.0.dist-info/licenses/LICENSE,sha256=mW62yNQJrqKgXhs8vhcQ97KRUOg6RpIMzncFlFOmPW4,1069
|
|
25
|
+
monkeyscode-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MonkeysCloud
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|