monkeybot-cli 0.2.1__tar.gz → 0.3.0__tar.gz
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.
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/.gitignore +1 -3
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/PKG-INFO +3 -2
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/pyproject.toml +4 -2
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/skills/monkeybot/SKILL.md +3 -1
- monkeybot_cli-0.3.0/src/monkeybot_cli/__init__.py +8 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/commands/chat.py +8 -1
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/commands/doctor.py +111 -11
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/commands/run_cmd.py +2 -3
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/commands/validate.py +2 -1
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/compat.py +1 -1
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/config_resolve.py +6 -12
- monkeybot_cli-0.3.0/src/monkeybot_cli/gateway_health.py +71 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold.py +27 -51
- monkeybot_cli-0.3.0/src/monkeybot_cli/scaffold_defaults/Dockerfile +27 -0
- monkeybot_cli-0.3.0/src/monkeybot_cli/scaffold_defaults/browser/SKILL.md +15 -0
- monkeybot_cli-0.3.0/src/monkeybot_cli/scaffold_defaults/dockerignore +5 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold_defaults/env.example +1 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold_defaults/mcp.json +3 -6
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold_defaults/monkeybot.example.yaml +5 -2
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_errors.py +1 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_cli.py +64 -7
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_config_resolve.py +12 -1
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_extras_catalog.py +1 -1
- monkeybot_cli-0.3.0/tests/test_gateway_health.py +135 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_opensandbox_lifecycle.py +2 -2
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_run_cmd.py +3 -2
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_scaffold.py +15 -10
- monkeybot_cli-0.2.1/src/monkeybot_cli/__init__.py +0 -3
- monkeybot_cli-0.2.1/src/monkeybot_cli/gateway_health.py +0 -34
- monkeybot_cli-0.2.1/src/monkeybot_cli/scaffold_defaults/setup-workspace.sh +0 -24
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/skills/monkeybot/references/config-sections.md +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/chat_renderer.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/chat_session.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/chat_status_bar.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/chat_theme.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/chat_tool_display.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/chat_tui.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/chat_tui_widgets.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/commands/__init__.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/commands/loop.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/commands/new.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/commands/talk.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/exit_commands.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/extras_catalog.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/main.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/opensandbox_lifecycle.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/output.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/providers.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/__init__.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/audio_io.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/client.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/gateway_manager.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/push_to_talk.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/session.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/session_controller.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/talk_ui.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/realtime/wire_encode.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/runtime_python.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold_defaults/AGENT.md +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold_defaults/__init__.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold_defaults/command_allowlist.yaml +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold_defaults/otel-collector.example.yaml +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/scaffold_defaults/permissions.yaml +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/session_controller.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/src/monkeybot_cli/terminal_markdown.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_e2e.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_grounding.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_renderer_parity.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_session.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_status_bar.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_theme.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_tool_display.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_chat_tui.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_doctor.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_realtime_session_controller.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_runtime_python.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/tests/test_terminal_markdown.py +0 -0
- {monkeybot_cli-0.2.1 → monkeybot_cli-0.3.0}/uv.lock +0 -0
|
@@ -9,9 +9,6 @@ internal/
|
|
|
9
9
|
# Local scaffolded harness config (defaults ship in monkeybot-cli scaffold_defaults/)
|
|
10
10
|
/monkeybot_config/
|
|
11
11
|
|
|
12
|
-
# Demo agent live config (materialized from monkeybot_config_example/ by demo_agent/run.sh)
|
|
13
|
-
demo_agent/monkeybot_config/
|
|
14
|
-
|
|
15
12
|
# Live eval run artifacts (only evals/baselines/ is versioned)
|
|
16
13
|
/evals/runs/
|
|
17
14
|
|
|
@@ -40,6 +37,7 @@ data/
|
|
|
40
37
|
htmlcov/
|
|
41
38
|
.mypy_cache/
|
|
42
39
|
.ruff_cache/
|
|
40
|
+
.deepeval/
|
|
43
41
|
|
|
44
42
|
# macOS
|
|
45
43
|
.DS_Store
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: monkeybot-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: CLI to create, configure, validate, and chat with monkeybot agents.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: httpx>=0.27.0
|
|
7
|
-
Requires-Dist: monkeybot
|
|
7
|
+
Requires-Dist: monkeybot-browser-mcp<1,>=0.2.0
|
|
8
|
+
Requires-Dist: monkeybot[cli]<3,>=2.2.0
|
|
8
9
|
Requires-Dist: python-dotenv>=1.0.0
|
|
9
10
|
Requires-Dist: pyyaml>=6.0.2
|
|
10
11
|
Requires-Dist: textual>=8.2.8
|
|
@@ -4,13 +4,14 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "monkeybot-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "CLI to create, configure, validate, and chat with monkeybot agents."
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
dependencies = [
|
|
11
11
|
# `cli` pulls realtime + typer so `monkeybot talk` works out of the box (text mode).
|
|
12
12
|
# Audio mic/PTT needs the agent (or this env) to also install monkeybot[cli-realtime].
|
|
13
|
-
"monkeybot[cli]>=2.
|
|
13
|
+
"monkeybot[cli]>=2.2.0,<3",
|
|
14
|
+
"monkeybot-browser-mcp>=0.2.0,<1",
|
|
14
15
|
"httpx>=0.27.0",
|
|
15
16
|
"pyyaml>=6.0.2",
|
|
16
17
|
"python-dotenv>=1.0.0",
|
|
@@ -22,6 +23,7 @@ monkeybot = "monkeybot_cli.main:main"
|
|
|
22
23
|
|
|
23
24
|
[tool.uv.sources]
|
|
24
25
|
monkeybot = { path = "..", editable = true }
|
|
26
|
+
monkeybot-browser-mcp = { path = "../integrations/browser-mcp", editable = true }
|
|
25
27
|
|
|
26
28
|
[dependency-groups]
|
|
27
29
|
dev = [
|
|
@@ -85,7 +85,9 @@ monkeybot new --dest /path/to/bot --provider openai --yes
|
|
|
85
85
|
# monkeybot new --dest /path/to/bot --provider openai --with postgres,sandbox --yes
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
Creates `monkeybot_config/`,
|
|
88
|
+
Creates `monkeybot_config/`, read-only `skills/`, writable `workspace/`,
|
|
89
|
+
`data/memory/`, `.env.example`, a Dockerfile, and an agent `pyproject.toml`.
|
|
90
|
+
Use `--force` only when overwriting is explicitly requested.
|
|
89
91
|
|
|
90
92
|
Then:
|
|
91
93
|
|
|
@@ -39,7 +39,10 @@ from monkeybot_cli.config_resolve import (
|
|
|
39
39
|
resolve_agent_root,
|
|
40
40
|
resolve_config,
|
|
41
41
|
)
|
|
42
|
-
from monkeybot_cli.gateway_health import
|
|
42
|
+
from monkeybot_cli.gateway_health import (
|
|
43
|
+
occupied_gateway_message as _occupied_gateway_message,
|
|
44
|
+
wait_for_health as _wait_for_health,
|
|
45
|
+
)
|
|
43
46
|
from monkeybot_cli.opensandbox_lifecycle import (
|
|
44
47
|
ensure_opensandbox_for_agent,
|
|
45
48
|
is_sandbox_enabled,
|
|
@@ -713,6 +716,10 @@ def run_chat(args: argparse.Namespace) -> int:
|
|
|
713
716
|
file=sys.stderr,
|
|
714
717
|
)
|
|
715
718
|
port = args.port if args.port else _port_from_config(config_path)
|
|
719
|
+
occupied = _occupied_gateway_message(base, port)
|
|
720
|
+
if occupied is not None:
|
|
721
|
+
print(occupied, file=sys.stderr)
|
|
722
|
+
return 1
|
|
716
723
|
spawned = _spawn_gateway(config_path, agent_root, port)
|
|
717
724
|
proc = spawned.proc
|
|
718
725
|
if not _wait_for_health(base, proc):
|
|
@@ -5,34 +5,27 @@ from __future__ import annotations
|
|
|
5
5
|
import argparse
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
-
import
|
|
8
|
+
import shlex
|
|
9
9
|
import subprocess
|
|
10
10
|
import tomllib
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
13
|
import httpx
|
|
14
14
|
|
|
15
|
+
from monkeybot.core.layout import AgentLayout, bootstrap_agent_layout
|
|
16
|
+
from monkeybot.core.tools.sandbox_executor import SandboxConfig
|
|
15
17
|
from monkeybot_cli.config_resolve import (
|
|
16
18
|
load_agent_dotenv,
|
|
17
19
|
load_config_doc,
|
|
18
20
|
resolve_agent_root,
|
|
19
21
|
resolve_config,
|
|
20
22
|
)
|
|
23
|
+
from monkeybot_cli.gateway_health import port_free as _port_free
|
|
21
24
|
from monkeybot_cli.output import CommandReport, check
|
|
22
25
|
from monkeybot_cli.providers import credentials_present, extra_module, spec_for_provider
|
|
23
26
|
from monkeybot_cli.runtime_python import resolve_runtime_python, run_probe
|
|
24
27
|
|
|
25
28
|
|
|
26
|
-
def _port_free(port: int) -> bool:
|
|
27
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
28
|
-
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
29
|
-
try:
|
|
30
|
-
s.bind(("127.0.0.1", port))
|
|
31
|
-
return True
|
|
32
|
-
except OSError:
|
|
33
|
-
return False
|
|
34
|
-
|
|
35
|
-
|
|
36
29
|
def _runtime_python_version(runtime, agent_root: Path) -> tuple[int, int, int]:
|
|
37
30
|
"""Ask the runtime interpreter for its version (major, minor, micro)."""
|
|
38
31
|
code = "import sys; print(sys.version_info[0], sys.version_info[1], sys.version_info[2])"
|
|
@@ -88,6 +81,105 @@ def _extra_remediation(extra: str, agent_root: Path, runtime) -> str:
|
|
|
88
81
|
)
|
|
89
82
|
|
|
90
83
|
|
|
84
|
+
def _storage_kind(uri: str) -> str:
|
|
85
|
+
"""Render storage kind without leaking URL credentials or filesystem details."""
|
|
86
|
+
if "://" not in uri:
|
|
87
|
+
return "unknown"
|
|
88
|
+
scheme, remainder = uri.split("://", 1)
|
|
89
|
+
if scheme == "sqlite":
|
|
90
|
+
return "sqlite"
|
|
91
|
+
host = remainder.split("@")[-1].split("/")[0]
|
|
92
|
+
return f"{scheme}://{host}" if host else scheme
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _add_layout_checks(report: CommandReport, layout: AgentLayout) -> None:
|
|
96
|
+
check(
|
|
97
|
+
report,
|
|
98
|
+
id="layout.resolved",
|
|
99
|
+
category="layout",
|
|
100
|
+
severity="error",
|
|
101
|
+
passed=True,
|
|
102
|
+
message="Canonical agent layout resolved",
|
|
103
|
+
value={
|
|
104
|
+
"agent_root": str(layout.agent_root),
|
|
105
|
+
"workspace": str(layout.workspace_root),
|
|
106
|
+
"skills": str(layout.skills_path),
|
|
107
|
+
"data": str(layout.data_root),
|
|
108
|
+
"db": _storage_kind(layout.db_url),
|
|
109
|
+
"memory": _storage_kind(layout.memory_storage_uri),
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
legacy_skills = layout.workspace_root / "skills"
|
|
114
|
+
populated_skills = layout.skills_path.exists() and any(layout.skills_path.iterdir())
|
|
115
|
+
legacy_exists = legacy_skills.exists() or legacy_skills.is_symlink()
|
|
116
|
+
collision = legacy_exists and populated_skills
|
|
117
|
+
source = str(legacy_skills)
|
|
118
|
+
destination = str(layout.skills_path)
|
|
119
|
+
check(
|
|
120
|
+
report,
|
|
121
|
+
id="layout.legacy_nested_skills",
|
|
122
|
+
category="layout",
|
|
123
|
+
severity="warning",
|
|
124
|
+
passed=not legacy_exists,
|
|
125
|
+
message=(
|
|
126
|
+
"Legacy workspace/skills detected"
|
|
127
|
+
+ ("; destination skills/ is populated (resolve collision manually)" if collision else "")
|
|
128
|
+
if legacy_exists
|
|
129
|
+
else "No legacy nested skills directory"
|
|
130
|
+
),
|
|
131
|
+
remediation=(
|
|
132
|
+
"Collision detected: do not move automatically; reconcile the two skill trees first."
|
|
133
|
+
if collision
|
|
134
|
+
else (
|
|
135
|
+
"Preview only (not executed): "
|
|
136
|
+
f"mv {shlex.quote(source)} {shlex.quote(destination)}"
|
|
137
|
+
)
|
|
138
|
+
if legacy_exists
|
|
139
|
+
else None
|
|
140
|
+
),
|
|
141
|
+
value=(
|
|
142
|
+
{
|
|
143
|
+
"source": source,
|
|
144
|
+
"destination": destination,
|
|
145
|
+
"collision": collision,
|
|
146
|
+
"action": None if collision else "mv",
|
|
147
|
+
}
|
|
148
|
+
if legacy_exists
|
|
149
|
+
else None
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
browser_enabled = False
|
|
154
|
+
if layout.mcp_config_path.is_file():
|
|
155
|
+
try:
|
|
156
|
+
servers = json.loads(layout.mcp_config_path.read_text(encoding="utf-8")).get("mcpServers", {})
|
|
157
|
+
browser = servers.get("browser", {}) if isinstance(servers, dict) else {}
|
|
158
|
+
browser_enabled = bool(browser.get("enabled")) if isinstance(browser, dict) else False
|
|
159
|
+
except (OSError, json.JSONDecodeError):
|
|
160
|
+
pass
|
|
161
|
+
check(
|
|
162
|
+
report,
|
|
163
|
+
id="browser.bundled",
|
|
164
|
+
category="layout",
|
|
165
|
+
severity="warning",
|
|
166
|
+
passed=True,
|
|
167
|
+
message="browser: enabled" if browser_enabled else "browser: disabled (bundled)",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
sandbox = SandboxConfig.from_env()
|
|
171
|
+
sandbox_mode = "remote (compute-only)" if not sandbox.shared_filesystem else "shared-filesystem"
|
|
172
|
+
check(
|
|
173
|
+
report,
|
|
174
|
+
id="sandbox.status",
|
|
175
|
+
category="layout",
|
|
176
|
+
severity="warning",
|
|
177
|
+
passed=True,
|
|
178
|
+
message=f"sandbox: {'enabled' if sandbox.enabled else 'disabled'} ({sandbox_mode})",
|
|
179
|
+
value={"image": sandbox.image, "mode": sandbox_mode},
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
91
183
|
def run_doctor(args: argparse.Namespace) -> int:
|
|
92
184
|
cwd = Path(args.cwd).expanduser().resolve() if args.cwd else None
|
|
93
185
|
config_path = resolve_config(args.config, cwd=cwd)
|
|
@@ -97,6 +189,14 @@ def run_doctor(args: argparse.Namespace) -> int:
|
|
|
97
189
|
report.config_path = str(config_path.resolve())
|
|
98
190
|
|
|
99
191
|
agent_root = resolve_agent_root(cwd=cwd, config_path=config_path)
|
|
192
|
+
layout = bootstrap_agent_layout(cwd=agent_root, config_path=config_path)
|
|
193
|
+
_add_layout_checks(report, layout)
|
|
194
|
+
if not args.json:
|
|
195
|
+
print("layout:")
|
|
196
|
+
print(f" agent_root: {layout.agent_root}")
|
|
197
|
+
print(f" workspace: {layout.workspace_root}")
|
|
198
|
+
print(f" skills: {layout.skills_path}")
|
|
199
|
+
print(f" data: {layout.data_root}")
|
|
100
200
|
runtime = resolve_runtime_python(agent_root)
|
|
101
201
|
|
|
102
202
|
py_version = _runtime_python_version(runtime, agent_root)
|
|
@@ -20,9 +20,8 @@ def run_run(args: argparse.Namespace) -> int:
|
|
|
20
20
|
env["MONKEYBOT_CONFIG"] = str(config_path)
|
|
21
21
|
if args.port:
|
|
22
22
|
env["PORT"] = str(args.port)
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# `monkeybot doctor`. Falls back to cwd when neither is provided.
|
|
23
|
+
# --cwd intentionally pins the subprocess and runtime directory. Without
|
|
24
|
+
# it, an explicit config determines the agent root.
|
|
26
25
|
agent_root = resolve_agent_root(cwd=cwd, config_path=config_path)
|
|
27
26
|
runtime = resolve_runtime_python(agent_root)
|
|
28
27
|
cmd = gateway_argv(runtime)
|
|
@@ -20,6 +20,7 @@ from monkeybot.core.config import (
|
|
|
20
20
|
from monkeybot.core.config.runtime_env import ENV_MAP
|
|
21
21
|
from monkeybot.core.config.settings import ConfigError, normalize_model_provider
|
|
22
22
|
from monkeybot.core.config.yaml_loader import load_monkeybot_yaml_dict
|
|
23
|
+
from monkeybot.core.layout import resolve_agent_root
|
|
23
24
|
|
|
24
25
|
from monkeybot_cli.config_resolve import load_agent_dotenv, resolve_config
|
|
25
26
|
from monkeybot_cli.output import CommandReport, check
|
|
@@ -84,7 +85,7 @@ def run_validate(args: argparse.Namespace) -> int:
|
|
|
84
85
|
return report.emit(as_json=args.json)
|
|
85
86
|
|
|
86
87
|
report.config_path = str(config_path.resolve())
|
|
87
|
-
base = config_path
|
|
88
|
+
base = resolve_agent_root(config_path=config_path)
|
|
88
89
|
|
|
89
90
|
try:
|
|
90
91
|
raw = config_path.read_text(encoding="utf-8")
|
|
@@ -9,18 +9,14 @@ from monkeybot.core.config.yaml_loader import (
|
|
|
9
9
|
load_monkeybot_yaml_dict,
|
|
10
10
|
resolve_monkeybot_config_path,
|
|
11
11
|
)
|
|
12
|
+
from monkeybot.core.layout import resolve_agent_root as _resolve_agent_root
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def resolve_agent_root(*, cwd: Path | None = None, config_path: Path | None = None) -> Path:
|
|
15
16
|
"""Return the agent project root (directory containing monkeybot_config/ or .env)."""
|
|
16
17
|
if cwd is not None:
|
|
17
18
|
return cwd.expanduser().resolve()
|
|
18
|
-
|
|
19
|
-
cp = config_path.expanduser().resolve()
|
|
20
|
-
if cp.parent.name == "monkeybot_config":
|
|
21
|
-
return cp.parent.parent
|
|
22
|
-
return cp.parent
|
|
23
|
-
return Path.cwd()
|
|
19
|
+
return _resolve_agent_root(config_path=config_path)
|
|
24
20
|
|
|
25
21
|
|
|
26
22
|
def load_agent_dotenv(*, cwd: Path | None = None, config_path: Path | None = None) -> Path | None:
|
|
@@ -34,10 +30,6 @@ def load_agent_dotenv(*, cwd: Path | None = None, config_path: Path | None = Non
|
|
|
34
30
|
if env_file.is_file():
|
|
35
31
|
load_dotenv(env_file, override=False)
|
|
36
32
|
return env_file
|
|
37
|
-
fallback = Path.cwd() / ".env"
|
|
38
|
-
if fallback.is_file() and fallback != env_file:
|
|
39
|
-
load_dotenv(fallback, override=False)
|
|
40
|
-
return fallback
|
|
41
33
|
return None
|
|
42
34
|
|
|
43
35
|
|
|
@@ -46,8 +38,10 @@ def resolve_config(explicit: str | None, *, cwd: Path | None = None) -> Path | N
|
|
|
46
38
|
if explicit:
|
|
47
39
|
p = Path(explicit).expanduser()
|
|
48
40
|
return p.resolve() if p.is_file() else None
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
if cwd is not None:
|
|
42
|
+
candidate = cwd.expanduser().resolve() / "monkeybot_config" / "monkeybot.yaml"
|
|
43
|
+
return candidate.resolve() if candidate.is_file() else None
|
|
44
|
+
return resolve_monkeybot_config_path()
|
|
51
45
|
|
|
52
46
|
|
|
53
47
|
def load_config_doc(config_path: str | Path | None = None) -> tuple[Path | None, dict]:
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Gateway health polling helpers shared by chat and talk."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import socket
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def port_free(port: int, host: str = "127.0.0.1") -> bool:
|
|
13
|
+
"""Return True when ``host:port`` can be bound (nothing listening)."""
|
|
14
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
15
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
16
|
+
try:
|
|
17
|
+
s.bind((host, port))
|
|
18
|
+
return True
|
|
19
|
+
except OSError:
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def wait_for_health(
|
|
24
|
+
base: str, proc: subprocess.Popen[str] | None, timeout_s: float = 30.0
|
|
25
|
+
) -> bool:
|
|
26
|
+
"""Poll ``GET {base}/health`` until 200 from *our* child, process dies, or timeout.
|
|
27
|
+
|
|
28
|
+
On success we re-check ``proc.poll()`` so a pre-existing listener on the same
|
|
29
|
+
port cannot satisfy health while our spawn has already exited (failed bind).
|
|
30
|
+
"""
|
|
31
|
+
deadline = time.monotonic() + timeout_s
|
|
32
|
+
while time.monotonic() < deadline:
|
|
33
|
+
if proc is not None and proc.poll() is not None:
|
|
34
|
+
return False
|
|
35
|
+
try:
|
|
36
|
+
resp = httpx.get(f"{base}/health", timeout=2.0)
|
|
37
|
+
if resp.status_code == 200:
|
|
38
|
+
if proc is not None and proc.poll() is not None:
|
|
39
|
+
return False
|
|
40
|
+
return True
|
|
41
|
+
except httpx.HTTPError:
|
|
42
|
+
pass
|
|
43
|
+
time.sleep(0.3)
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def health_ok(base: str) -> bool:
|
|
48
|
+
"""Single-shot health check."""
|
|
49
|
+
try:
|
|
50
|
+
return httpx.get(f"{base}/health", timeout=2.0).status_code == 200
|
|
51
|
+
except httpx.HTTPError:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def occupied_gateway_message(base: str, port: int) -> str | None:
|
|
56
|
+
"""If ``port`` is already taken, return a user-facing error; else ``None``.
|
|
57
|
+
|
|
58
|
+
Used before ``monkeybot chat`` spawns a gateway so we never talk to a stale
|
|
59
|
+
process while the status bar shows the current yaml provider/model.
|
|
60
|
+
"""
|
|
61
|
+
listening = not port_free(port)
|
|
62
|
+
healthy = health_ok(base)
|
|
63
|
+
if not listening and not healthy:
|
|
64
|
+
return None
|
|
65
|
+
detail = " (gateway /health OK)" if healthy else ""
|
|
66
|
+
return (
|
|
67
|
+
f"Port {port} is already in use{detail}. "
|
|
68
|
+
f"Another monkeybot process is likely still running at {base}.\n"
|
|
69
|
+
"Exit that session with /bye, stop `monkeybot run`, or free the port, "
|
|
70
|
+
"then retry — or connect with `monkeybot chat --attach`."
|
|
71
|
+
)
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
|
-
import shutil
|
|
7
|
-
import stat
|
|
8
6
|
from importlib import resources
|
|
9
7
|
from importlib.resources.abc import Traversable
|
|
10
8
|
from pathlib import Path
|
|
@@ -26,12 +24,11 @@ _CONFIG_BUNDLE: Final[tuple[tuple[str, str], ...]] = (
|
|
|
26
24
|
("command_allowlist.yaml", "command_allowlist.yaml"),
|
|
27
25
|
("permissions.yaml", "permissions.yaml"),
|
|
28
26
|
("AGENT.md", "AGENT.md"),
|
|
29
|
-
("env.example", "env.example"),
|
|
30
27
|
("otel-collector.example.yaml", "otel-collector.example.yaml"),
|
|
31
28
|
)
|
|
32
29
|
|
|
33
30
|
_MEMORY_INDEX: Final = (
|
|
34
|
-
"# Memory
|
|
31
|
+
"# Memory Index\n\nAdd sections here or let memory tools populate this file.\n"
|
|
35
32
|
)
|
|
36
33
|
|
|
37
34
|
|
|
@@ -115,7 +112,7 @@ def ensure_memory(dest: Path, *, force: bool) -> list[str]:
|
|
|
115
112
|
|
|
116
113
|
|
|
117
114
|
def ensure_workspace(dest: Path, *, force: bool) -> list[str]:
|
|
118
|
-
"""Create workspace
|
|
115
|
+
"""Create the writable workspace and a separate trusted skills root."""
|
|
119
116
|
lines: list[str] = []
|
|
120
117
|
workspace = dest / "workspace"
|
|
121
118
|
workspace.mkdir(parents=True, exist_ok=True)
|
|
@@ -129,52 +126,14 @@ def ensure_workspace(dest: Path, *, force: bool) -> list[str]:
|
|
|
129
126
|
lines.append(" workspace/.gitkeep: skipped")
|
|
130
127
|
|
|
131
128
|
dest.joinpath("skills").mkdir(parents=True, exist_ok=True)
|
|
132
|
-
link = workspace / "skills"
|
|
133
|
-
expected = (dest / "skills").resolve()
|
|
134
|
-
|
|
135
|
-
if link.is_symlink():
|
|
136
|
-
if link.resolve() == expected:
|
|
137
|
-
lines.append(" workspace/skills: skipped (symlink ok)")
|
|
138
|
-
return lines
|
|
139
|
-
link.unlink()
|
|
140
|
-
|
|
141
|
-
if link.exists() and not link.is_symlink():
|
|
142
|
-
if not force:
|
|
143
|
-
lines.append(" workspace/skills: skipped (path exists)")
|
|
144
|
-
return lines
|
|
145
|
-
if link.is_dir():
|
|
146
|
-
shutil.rmtree(link)
|
|
147
|
-
else:
|
|
148
|
-
link.unlink()
|
|
149
|
-
|
|
150
|
-
try:
|
|
151
|
-
link.symlink_to("../skills", target_is_directory=True)
|
|
152
|
-
lines.append(" workspace/skills: symlink -> ../skills")
|
|
153
|
-
except OSError:
|
|
154
|
-
readme = workspace / "SKILLS_README.txt"
|
|
155
|
-
readme.write_text(
|
|
156
|
-
"Could not create workspace/skills symlink on this platform.\n"
|
|
157
|
-
"Run: bash scripts/setup-workspace.sh\n"
|
|
158
|
-
"Or copy/symlink skills/ into workspace/skills manually.\n",
|
|
159
|
-
encoding="utf-8",
|
|
160
|
-
)
|
|
161
|
-
lines.append(" workspace/skills: symlink failed (see workspace/SKILLS_README.txt)")
|
|
162
|
-
|
|
163
129
|
return lines
|
|
164
130
|
|
|
165
131
|
|
|
166
|
-
def
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return "skipped"
|
|
172
|
-
existed = dest_script.exists()
|
|
173
|
-
dest_script.write_bytes(
|
|
174
|
-
(resources.files(_DEFAULTS_PKG) / "setup-workspace.sh").read_bytes()
|
|
175
|
-
)
|
|
176
|
-
dest_script.chmod(dest_script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
177
|
-
return "overwritten" if existed else "created"
|
|
132
|
+
def install_browser_skill(dest: Path, *, force: bool) -> str:
|
|
133
|
+
"""Install the bundled, trusted browser procedure into ``skills/browser``."""
|
|
134
|
+
target = dest / "skills" / "browser" / "SKILL.md"
|
|
135
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
return _install_file(target, resources.files(_DEFAULTS_PKG) / "browser" / "SKILL.md", force=force)
|
|
178
137
|
|
|
179
138
|
|
|
180
139
|
def install_env_example(dest: Path, *, force: bool) -> str:
|
|
@@ -186,6 +145,17 @@ def install_env_example(dest: Path, *, force: bool) -> str:
|
|
|
186
145
|
return "overwritten" if existed else "created"
|
|
187
146
|
|
|
188
147
|
|
|
148
|
+
def install_container_files(dest: Path, *, force: bool) -> list[str]:
|
|
149
|
+
"""Install a deployable agent image definition, never the repo demo image."""
|
|
150
|
+
files = (("Dockerfile", "Dockerfile"), ("dockerignore", ".dockerignore"))
|
|
151
|
+
lines: list[str] = []
|
|
152
|
+
for source, target_name in files:
|
|
153
|
+
target = dest / target_name
|
|
154
|
+
status = _install_file(target, resources.files(_DEFAULTS_PKG) / source, force=force)
|
|
155
|
+
lines.append(f" {target_name}: {status}")
|
|
156
|
+
return lines
|
|
157
|
+
|
|
158
|
+
|
|
189
159
|
def _sanitize_project_name(raw: str) -> str:
|
|
190
160
|
name = re.sub(r"[^a-z0-9._-]+", "-", raw.strip().lower()).strip("-._")
|
|
191
161
|
return name or "agent"
|
|
@@ -244,7 +214,9 @@ def write_agent_pyproject(
|
|
|
244
214
|
if path.exists() and not force:
|
|
245
215
|
return "skipped"
|
|
246
216
|
existed = path.exists()
|
|
247
|
-
|
|
217
|
+
# Sandbox is configured in every generated agent, so install its SDK with
|
|
218
|
+
# the provider dependencies rather than imposing it on every core install.
|
|
219
|
+
dep = monkeybot_requirement(provider=provider, extras=["sandbox", *(extras or [])])
|
|
248
220
|
name = _sanitize_project_name(dest.name)
|
|
249
221
|
path.write_text(
|
|
250
222
|
(
|
|
@@ -254,7 +226,10 @@ def write_agent_pyproject(
|
|
|
254
226
|
'requires-python = ">=3.11"\n'
|
|
255
227
|
"dependencies = [\n"
|
|
256
228
|
f' "{dep}",\n'
|
|
229
|
+
' "monkeybot-browser-mcp>=0.2.0,<1",\n'
|
|
257
230
|
"]\n"
|
|
231
|
+
"\n[tool.uv]\n"
|
|
232
|
+
"package = false\n"
|
|
258
233
|
),
|
|
259
234
|
encoding="utf-8",
|
|
260
235
|
)
|
|
@@ -269,7 +244,7 @@ def run_new(
|
|
|
269
244
|
model: str | None = None,
|
|
270
245
|
extras: list[str] | None = None,
|
|
271
246
|
) -> list[str]:
|
|
272
|
-
"""Full scaffold: config bundle,
|
|
247
|
+
"""Full scaffold: config bundle, trusted skills, writable state, and image files."""
|
|
273
248
|
cfg_dir = dest / "monkeybot_config"
|
|
274
249
|
report = install_config_bundle(cfg_dir, force=force)
|
|
275
250
|
report.append(
|
|
@@ -278,8 +253,9 @@ def run_new(
|
|
|
278
253
|
)
|
|
279
254
|
report.extend(ensure_memory(dest, force=force))
|
|
280
255
|
report.extend(ensure_workspace(dest, force=force))
|
|
281
|
-
report.append(f"
|
|
256
|
+
report.append(f" skills/browser/SKILL.md: {install_browser_skill(dest, force=force)}")
|
|
282
257
|
report.append(f" .env.example: {install_env_example(dest, force=force)}")
|
|
258
|
+
report.extend(install_container_files(dest, force=force))
|
|
283
259
|
report.append(
|
|
284
260
|
f" pyproject.toml: "
|
|
285
261
|
f"{write_agent_pyproject(dest, provider=provider, extras=extras, force=force)}"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
FROM python:3.12-slim
|
|
2
|
+
|
|
3
|
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|
4
|
+
WORKDIR /agent
|
|
5
|
+
|
|
6
|
+
COPY pyproject.toml uv.lock ./
|
|
7
|
+
RUN uv sync --frozen --no-dev
|
|
8
|
+
|
|
9
|
+
ARG INSTALL_CHROMIUM=0
|
|
10
|
+
USER root
|
|
11
|
+
RUN if [ "$INSTALL_CHROMIUM" = "1" ]; then \
|
|
12
|
+
apt-get update && apt-get install -y --no-install-recommends chromium \
|
|
13
|
+
&& rm -rf /var/lib/apt/lists/*; \
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
COPY monkeybot_config ./monkeybot_config
|
|
17
|
+
COPY skills ./skills
|
|
18
|
+
RUN useradd --create-home agent \
|
|
19
|
+
&& mkdir -p workspace data/memory \
|
|
20
|
+
&& chown -R agent:agent workspace data
|
|
21
|
+
|
|
22
|
+
USER agent
|
|
23
|
+
ENV PATH="/agent/.venv/bin:$PATH" PORT=8080
|
|
24
|
+
EXPOSE 8080
|
|
25
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
26
|
+
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health')"
|
|
27
|
+
CMD ["python", "-m", "monkeybot.gateway.main"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: browser
|
|
3
|
+
description: Control a real browser through the optional bundled browser MCP server.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Browser
|
|
7
|
+
|
|
8
|
+
When browser tools are needed, call `enable_mcp("browser")` first. The server is
|
|
9
|
+
bundled with every scaffold but is disabled by default in `monkeybot_config/mcp.json`.
|
|
10
|
+
|
|
11
|
+
Use indexed DOM tools before screenshots. Browser screenshots and agent-written
|
|
12
|
+
playbooks are workspace data: `browser/Screenshots/` and `browser/playbooks/`.
|
|
13
|
+
They are not trusted skills and may be discarded with an ephemeral workspace.
|
|
14
|
+
|
|
15
|
+
Finish browser sessions with `browser_stop`.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Copy to .env and fill in secrets / machine-local values only.
|
|
2
2
|
|
|
3
3
|
# Conversation history — SQLite is fine for single-agent dev.
|
|
4
|
+
# Relative paths in monkeybot.yaml are already anchored to this agent root.
|
|
4
5
|
DB_URL=sqlite:///data/monkeybot.db
|
|
5
6
|
# DB_URL=postgresql://postgres:postgres@localhost:5432/monkeybot
|
|
6
7
|
# DB_URL=firestore://your-gcp-project/(default)
|
|
@@ -30,18 +30,15 @@
|
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
32
|
"browser": {
|
|
33
|
-
"
|
|
33
|
+
"enabled": false,
|
|
34
|
+
"command": "${MONKEYBOT_PYTHON}",
|
|
34
35
|
"args": [
|
|
35
|
-
"run",
|
|
36
|
-
"--project",
|
|
37
|
-
"/path/to/monkeybot/integrations/browser-mcp",
|
|
38
|
-
"python",
|
|
39
36
|
"-m",
|
|
40
37
|
"browser_mcp.server"
|
|
41
38
|
],
|
|
42
39
|
"env": {
|
|
43
40
|
"BU_NAME": "monkeybot",
|
|
44
|
-
"BROWSER_MCP_PLAYBOOKS_DIR": "
|
|
41
|
+
"BROWSER_MCP_PLAYBOOKS_DIR": "${MONKEYBOT_WORKSPACE_ROOT}/browser/playbooks",
|
|
45
42
|
"BROWSER_MCP_SCREENSHOTS_DIR": "${MONKEYBOT_WORKSPACE_ROOT}/browser/Screenshots"
|
|
46
43
|
}
|
|
47
44
|
}
|