monkeybot-cli 0.9.0__tar.gz → 0.11.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.9.0 → monkeybot_cli-0.11.0}/.gitignore +5 -1
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/PKG-INFO +2 -2
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/pyproject.toml +2 -2
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/skills/monkeybot/references/config-sections.md +3 -2
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/chat_session.py +3 -9
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/chat_tool_display.py +1 -3
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/chat_tui.py +20 -28
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/commands/chat.py +2 -3
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/commands/doctor.py +64 -1
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/commands/loop.py +6 -1
- monkeybot_cli-0.11.0/src/monkeybot_cli/commands/mcp_login.py +81 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/commands/new.py +45 -7
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/commands/validate.py +41 -8
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/compat.py +2 -6
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/extras_catalog.py +1 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/gateway_health.py +1 -3
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/main.py +13 -1
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/opensandbox_lifecycle.py +4 -3
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/output.py +3 -1
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/providers.py +2 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/realtime/push_to_talk.py +1 -2
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/realtime/session_controller.py +1 -3
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/scaffold.py +86 -16
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/scaffold_defaults/command_allowlist.yaml +2 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/scaffold_defaults/env.example +3 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/scaffold_defaults/monkeybot.example.yaml +26 -2
- monkeybot_cli-0.11.0/src/monkeybot_cli/scaffold_defaults/templates/coding/AGENT.md +35 -0
- monkeybot_cli-0.11.0/src/monkeybot_cli/scaffold_defaults/templates/coding/command_allowlist.yaml +92 -0
- monkeybot_cli-0.11.0/src/monkeybot_cli/scaffold_defaults/templates/coding/monkeybot.overrides.yaml +31 -0
- monkeybot_cli-0.11.0/src/monkeybot_cli/scaffold_defaults/templates/coding/permissions.yaml +42 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_chat_continue.py +1 -3
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_chat_errors.py +12 -4
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_chat_local_shell.py +3 -1
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_chat_renderer_parity.py +1 -3
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_chat_session.py +1 -3
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_chat_tui.py +15 -18
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_cli.py +53 -3
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_config_resolve.py +3 -1
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_doctor.py +52 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_realtime_session_controller.py +1 -3
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_run_cmd.py +6 -2
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_runtime_python.py +3 -1
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_scaffold.py +193 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_terminal_markdown.py +1 -5
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/uv.lock +7 -3
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/skills/monkeybot/SKILL.md +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/__init__.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/chat_file_index.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/chat_local_shell.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/chat_renderer.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/chat_status_bar.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/chat_theme.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/chat_tui_widgets.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/commands/__init__.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/commands/refresh.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/commands/run_cmd.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/commands/talk.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/commands/trace.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/config_resolve.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/exit_commands.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/process_tree.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/realtime/__init__.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/realtime/audio_io.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/realtime/client.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/realtime/gateway_manager.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/realtime/session.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/realtime/talk_ui.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/realtime/wire_encode.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/runtime_python.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/scaffold_defaults/AGENT.md +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/scaffold_defaults/Dockerfile +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/scaffold_defaults/__init__.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/scaffold_defaults/browser/SKILL.md +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/scaffold_defaults/dockerignore +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/scaffold_defaults/goal/SKILL.md +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/scaffold_defaults/image-generator/SKILL.md +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/scaffold_defaults/image-generator/generate_image.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/scaffold_defaults/loop/SKILL.md +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/scaffold_defaults/mcp.json +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/scaffold_defaults/opensandbox.docker.toml +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/scaffold_defaults/otel-collector.example.yaml +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/scaffold_defaults/permissions.yaml +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/session_controller.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/src/monkeybot_cli/terminal_markdown.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_chat_e2e.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_chat_file_index.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_chat_grounding.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_chat_status_bar.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_chat_theme.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_chat_tool_display.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_extras_catalog.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_gateway_health.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_opensandbox_lifecycle.py +0 -0
- {monkeybot_cli-0.9.0 → monkeybot_cli-0.11.0}/tests/test_trace_cmd.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: monkeybot-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.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
7
|
Requires-Dist: monkeybot-browser-mcp<1,>=0.5.0
|
|
8
|
-
Requires-Dist: monkeybot[cli]<4,>=3.
|
|
8
|
+
Requires-Dist: monkeybot[cli]<4,>=3.10.0
|
|
9
9
|
Requires-Dist: python-dotenv>=1.0.0
|
|
10
10
|
Requires-Dist: pyyaml>=6.0.2
|
|
11
11
|
Requires-Dist: textual>=8.2.8
|
|
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "monkeybot-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.11.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]>=3.
|
|
13
|
+
"monkeybot[cli]>=3.10.0,<4",
|
|
14
14
|
"monkeybot-browser-mcp>=0.5.0,<1",
|
|
15
15
|
"httpx>=0.27.0",
|
|
16
16
|
"pyyaml>=6.0.2",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Deep reference for every `monkeybot.yaml` section. Load this only when a user needs to customize beyond Tier 1. The canonical, fully-commented template lives at `cli/src/monkeybot_cli/scaffold_defaults/monkeybot.example.yaml` in the monkeybot repo (copied to `monkeybot_config/monkeybot.example.yaml` when you scaffold); this file adds the **"when would I change this?"** context the comments don't.
|
|
4
4
|
|
|
5
|
-
**Precedence:** env vars and `.env` win over YAML for `ENV_MAP` keys. `model.*` (provider, name, temperature, max_tokens, thinking_budget, context_window, summarization_model, max_turns, cache_retention) is YAML-only — leftover `MODEL_*` env vars are ignored. The YAML→env mapping is `ENV_MAP` in `src/monkeybot/core/config/runtime_env.py`. If a YAML edit has no effect, check for a shadowing env var (except `model.*`).
|
|
5
|
+
**Precedence:** env vars and `.env` win over YAML for `ENV_MAP` keys. `model.*` (provider, name, temperature, max_tokens, thinking_budget, reasoning_effort, context_window, summarization_model, max_turns, cache_retention) is YAML-only — leftover `MODEL_*` env vars are ignored. The YAML→env mapping is `ENV_MAP` in `src/monkeybot/core/config/runtime_env.py`. If a YAML edit has no effect, check for a shadowing env var (except `model.*`).
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -42,7 +42,8 @@ YAML-only for provider, name, sampling, context window, summarization model, max
|
|
|
42
42
|
| `name` | `gemini-3-flash` | Pick a specific model id |
|
|
43
43
|
| `temperature` | `0.7` | Lower for deterministic output, higher for creative |
|
|
44
44
|
| `max_tokens` | `60000` | Cap per-response length |
|
|
45
|
-
| `thinking_budget` | `-1` | Gemini: `-1` model default, `0` off, `N` token budget. Ollama reasoning models: `-1` server default, `0` off (`reasoning_effort: none`) |
|
|
45
|
+
| `thinking_budget` | `-1` | Gemini: `-1` model default, `0` off, `N` token budget. Ollama reasoning models: `-1` server default, `0` off (`reasoning_effort: none`). Ignored when `reasoning_effort` is set on providers that take an effort level |
|
|
46
|
+
| `reasoning_effort` | (unset) | Effort level passed through to the provider (`low`, `medium`, `high`, `xhigh`, `max`, or whatever that provider accepts). Omit for the provider default. YAML-only. Gemini only sends SDK levels (`minimal`, `low`, `medium`, `high`) and otherwise uses `thinking_budget`. A level the API rejects is omitted on later turns. Bedrock sends it for Claude ids only; Converse models ignore it. Curator and summarizer calls skip the configured effort; when one is set, OpenAI and Ollama send `none` instead |
|
|
46
47
|
| `context_window` | `1000000` | Summarization trigger (tokens); also drives soft-spill / `read_file` char budgets. **Not** Ollama `num_ctx` |
|
|
47
48
|
| `keep_alive` | `24h` (ollama-local only) | How long local Ollama keeps the model (and KV prefix cache) loaded. YAML only. Set `"0"` to omit. See `docs/ollama-local.md` |
|
|
48
49
|
| `num_ctx` | (unset) | Optional pinned Ollama `num_ctx`. YAML only. Omit = server default. Do not copy `context_window` |
|
|
@@ -101,9 +101,7 @@ def extract_elicitation_fields(payload: dict[str, Any]) -> tuple[str | None, dic
|
|
|
101
101
|
else:
|
|
102
102
|
message = message.strip()
|
|
103
103
|
schema_raw = (
|
|
104
|
-
payload.get("requestedSchema")
|
|
105
|
-
or payload.get("requested_schema")
|
|
106
|
-
or payload.get("schema")
|
|
104
|
+
payload.get("requestedSchema") or payload.get("requested_schema") or payload.get("schema")
|
|
107
105
|
)
|
|
108
106
|
schema = dict(schema_raw) if isinstance(schema_raw, dict) else None
|
|
109
107
|
return message, schema
|
|
@@ -662,9 +660,7 @@ class ChatSessionController:
|
|
|
662
660
|
|
|
663
661
|
self._maybe_thinking_trace(evt, request_id)
|
|
664
662
|
|
|
665
|
-
def _on_context_usage_hint(
|
|
666
|
-
self, *, estimated_tokens: int, context_window_tokens: int
|
|
667
|
-
) -> None:
|
|
663
|
+
def _on_context_usage_hint(self, *, estimated_tokens: int, context_window_tokens: int) -> None:
|
|
668
664
|
self.usage.update_context_hint(
|
|
669
665
|
estimated_prompt_tokens=estimated_tokens,
|
|
670
666
|
context_window_tokens=context_window_tokens,
|
|
@@ -750,9 +746,7 @@ class ChatSessionController:
|
|
|
750
746
|
if answer.cancelled:
|
|
751
747
|
approved = False
|
|
752
748
|
reason = (
|
|
753
|
-
"cancelled by user"
|
|
754
|
-
if answer.cancelled
|
|
755
|
-
else (None if approved else "denied by user")
|
|
749
|
+
"cancelled by user" if answer.cancelled else (None if approved else "denied by user")
|
|
756
750
|
)
|
|
757
751
|
ok = await self._post_hitl(
|
|
758
752
|
f"{self.base}/sessions/{self.session_id}/tool-confirmations/{evt.tool_call_id}",
|
|
@@ -37,9 +37,7 @@ _SHELL_TAIL_LINES = 40
|
|
|
37
37
|
|
|
38
38
|
_SHELL_TOOLS = frozenset({"run_command", "execute", "shell", "bash"})
|
|
39
39
|
_READ_TOOLS = frozenset({"read_file", "read"})
|
|
40
|
-
_EDIT_TOOLS = frozenset(
|
|
41
|
-
{"write_file", "write", "edit_file", "apply_patch", "str_replace"}
|
|
42
|
-
)
|
|
40
|
+
_EDIT_TOOLS = frozenset({"write_file", "write", "edit_file", "apply_patch", "str_replace"})
|
|
43
41
|
_PATH_TOOLS = _READ_TOOLS | _EDIT_TOOLS
|
|
44
42
|
_SEARCH_TOOLS = frozenset({"grep", "web_search"})
|
|
45
43
|
_HINT_KEYS = frozenset(
|
|
@@ -93,6 +93,7 @@ def cycle_approval_mode(mode: str) -> str:
|
|
|
93
93
|
idx = 0
|
|
94
94
|
return _APPROVAL_MODES[(idx + 1) % len(_APPROVAL_MODES)]
|
|
95
95
|
|
|
96
|
+
|
|
96
97
|
_SLASH_SPECS: tuple[tuple[str, str], ...] = (
|
|
97
98
|
("/help", "Show commands and key hints"),
|
|
98
99
|
("/new", "Start a fresh session"),
|
|
@@ -327,11 +328,7 @@ class ChatApp(App[int]):
|
|
|
327
328
|
self.theme = resolve_theme_name(theme_choice)
|
|
328
329
|
self.base = base
|
|
329
330
|
self.agent_root = agent_root
|
|
330
|
-
self.config_path = (
|
|
331
|
-
config_path.expanduser().resolve()
|
|
332
|
-
if config_path is not None
|
|
333
|
-
else None
|
|
334
|
-
)
|
|
331
|
+
self.config_path = config_path.expanduser().resolve() if config_path is not None else None
|
|
335
332
|
self.provider = provider
|
|
336
333
|
self.model = model
|
|
337
334
|
self.spawned_gateway = spawned_gateway
|
|
@@ -443,9 +440,7 @@ class ChatApp(App[int]):
|
|
|
443
440
|
else:
|
|
444
441
|
parts.append("y approve · n deny · Ctrl-C cancel")
|
|
445
442
|
elif self.show_hints:
|
|
446
|
-
parts.append(
|
|
447
|
-
"Enter send · Ctrl+J newline · ↑ history · / commands · Ctrl+U usage"
|
|
448
|
-
)
|
|
443
|
+
parts.append("Enter send · Ctrl+J newline · ↑ history · / commands · Ctrl+U usage")
|
|
449
444
|
else:
|
|
450
445
|
parts.append("F1 hints")
|
|
451
446
|
return " · ".join(parts)
|
|
@@ -495,8 +490,7 @@ class ChatApp(App[int]):
|
|
|
495
490
|
self._session_cost_text = f"${usage.cost_usd:.4f}"
|
|
496
491
|
if self.show_usage:
|
|
497
492
|
self._usage_text = (
|
|
498
|
-
f"in={usage.input_tokens} out={usage.output_tokens} "
|
|
499
|
-
f"${usage.cost_usd:.4f}"
|
|
493
|
+
f"in={usage.input_tokens} out={usage.output_tokens} ${usage.cost_usd:.4f}"
|
|
500
494
|
)
|
|
501
495
|
|
|
502
496
|
def _hide_empty_hint(self) -> None:
|
|
@@ -556,6 +550,7 @@ class ChatApp(App[int]):
|
|
|
556
550
|
if transcript.is_anchored and not transcript._anchor_released:
|
|
557
551
|
transcript.release_anchor()
|
|
558
552
|
self._refresh_jump_button()
|
|
553
|
+
|
|
559
554
|
def _refresh_jump_button(self) -> None:
|
|
560
555
|
with contextlib.suppress(NoMatches):
|
|
561
556
|
btn = self.query_one("#jump-bottom", Button)
|
|
@@ -627,7 +622,12 @@ class ChatApp(App[int]):
|
|
|
627
622
|
if isinstance(child, EarlierTurns):
|
|
628
623
|
earlier = child
|
|
629
624
|
continue
|
|
630
|
-
if
|
|
625
|
+
if (
|
|
626
|
+
child is self._assistant
|
|
627
|
+
or child is self._thinking
|
|
628
|
+
or child is self._thinking_trace
|
|
629
|
+
or child is self._hitl_card
|
|
630
|
+
):
|
|
631
631
|
continue
|
|
632
632
|
if isinstance(child, ToolCallBlock) and (
|
|
633
633
|
child in self._open_tools.values() or child in self._anon_tools
|
|
@@ -683,6 +683,7 @@ class ChatApp(App[int]):
|
|
|
683
683
|
def _seal_thinking_trace(self) -> None:
|
|
684
684
|
"""Finish the current thinking block and start fresh on the next delta."""
|
|
685
685
|
self._finish_thinking_trace(clear=True)
|
|
686
|
+
|
|
686
687
|
def _mount_user(self, body: str) -> None:
|
|
687
688
|
self._mount(UserTurn(body, show_timestamp=self.show_timestamps))
|
|
688
689
|
|
|
@@ -768,9 +769,7 @@ class ChatApp(App[int]):
|
|
|
768
769
|
palette.display = False
|
|
769
770
|
palette.clear_options()
|
|
770
771
|
return
|
|
771
|
-
palette.set_options(
|
|
772
|
-
[Option(f"{cmd} {desc}", id=cmd) for cmd, desc in matches]
|
|
773
|
-
)
|
|
772
|
+
palette.set_options([Option(f"{cmd} {desc}", id=cmd) for cmd, desc in matches])
|
|
774
773
|
palette.highlighted = 0
|
|
775
774
|
palette.display = True
|
|
776
775
|
|
|
@@ -945,9 +944,7 @@ class ChatApp(App[int]):
|
|
|
945
944
|
if state == "reconnecting":
|
|
946
945
|
attempt = p.get("attempt")
|
|
947
946
|
self._conn_text = (
|
|
948
|
-
f"[yellow]reconnecting…[/] ({attempt})"
|
|
949
|
-
if attempt
|
|
950
|
-
else "[yellow]reconnecting…[/]"
|
|
947
|
+
f"[yellow]reconnecting…[/] ({attempt})" if attempt else "[yellow]reconnecting…[/]"
|
|
951
948
|
)
|
|
952
949
|
elif state == "connected":
|
|
953
950
|
self._conn_text = ""
|
|
@@ -1508,7 +1505,9 @@ class ChatApp(App[int]):
|
|
|
1508
1505
|
self.model = name
|
|
1509
1506
|
self._refresh_topbar()
|
|
1510
1507
|
await self._reset_transcript_ui()
|
|
1511
|
-
self._mount_system(
|
|
1508
|
+
self._mount_system(
|
|
1509
|
+
f"Model set to {provider}/{name} — new session started (context cleared)"
|
|
1510
|
+
)
|
|
1512
1511
|
self._refresh_status()
|
|
1513
1512
|
self.query_one("#prompt", Composer).focus()
|
|
1514
1513
|
|
|
@@ -1566,6 +1565,7 @@ class ChatApp(App[int]):
|
|
|
1566
1565
|
except Exception as exc: # noqa: BLE001 - surface any YAML/parse error to the user
|
|
1567
1566
|
self._mount_system(f"Could not read config: {exc}", error=True)
|
|
1568
1567
|
return
|
|
1568
|
+
|
|
1569
1569
|
def _section(key: str) -> dict[str, object]:
|
|
1570
1570
|
value = doc.get(key)
|
|
1571
1571
|
return value if isinstance(value, dict) else {}
|
|
@@ -1755,7 +1755,6 @@ class ChatApp(App[int]):
|
|
|
1755
1755
|
def action_toggle_usage(self) -> None:
|
|
1756
1756
|
self._cmd_usage()
|
|
1757
1757
|
|
|
1758
|
-
|
|
1759
1758
|
def action_toggle_ptt(self) -> None:
|
|
1760
1759
|
"""Space toggles in-TUI PTT when composer is empty (realtime / SSH)."""
|
|
1761
1760
|
with contextlib.suppress(NoMatches):
|
|
@@ -1786,10 +1785,7 @@ class ChatApp(App[int]):
|
|
|
1786
1785
|
self._controller.abort_turn()
|
|
1787
1786
|
return
|
|
1788
1787
|
now = time.monotonic()
|
|
1789
|
-
if
|
|
1790
|
-
self._last_esc is not None
|
|
1791
|
-
and now - self._last_esc <= _ESC_RECALL_WINDOW
|
|
1792
|
-
):
|
|
1788
|
+
if self._last_esc is not None and now - self._last_esc <= _ESC_RECALL_WINDOW:
|
|
1793
1789
|
self._last_esc = None
|
|
1794
1790
|
composer.recall_last()
|
|
1795
1791
|
return
|
|
@@ -1815,11 +1811,7 @@ class ChatApp(App[int]):
|
|
|
1815
1811
|
self._goodbye_and_exit()
|
|
1816
1812
|
|
|
1817
1813
|
def _goodbye_and_exit(self) -> None:
|
|
1818
|
-
msg =
|
|
1819
|
-
"Goodbye — shutting down gateway…"
|
|
1820
|
-
if self.spawned_gateway
|
|
1821
|
-
else "Goodbye."
|
|
1822
|
-
)
|
|
1814
|
+
msg = "Goodbye — shutting down gateway…" if self.spawned_gateway else "Goodbye."
|
|
1823
1815
|
self._mount_system(msg)
|
|
1824
1816
|
if self._session_id:
|
|
1825
1817
|
self._mount_system("To continue this conversation, run: monkeybot chat --continue")
|
|
@@ -64,6 +64,7 @@ _UNDERLINE = "\x1b[4m"
|
|
|
64
64
|
_SPINNER_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
|
65
65
|
_GROUNDING_SOURCES_MAX = 5
|
|
66
66
|
|
|
67
|
+
|
|
67
68
|
def _hyperlink(url: str, label: str) -> str:
|
|
68
69
|
"""OSC 8 terminal hyperlink; falls back to a plain URL on non-TTY streams."""
|
|
69
70
|
if not url:
|
|
@@ -278,9 +279,7 @@ def _resolve_base_url(args: argparse.Namespace, config_path: Path | None) -> str
|
|
|
278
279
|
return f"http://127.0.0.1:{port}"
|
|
279
280
|
|
|
280
281
|
|
|
281
|
-
def _model_banner_fields(
|
|
282
|
-
args: argparse.Namespace, config_path: Path | None
|
|
283
|
-
) -> tuple[str, str]:
|
|
282
|
+
def _model_banner_fields(args: argparse.Namespace, config_path: Path | None) -> tuple[str, str]:
|
|
284
283
|
provider = (args.model_provider or "").strip()
|
|
285
284
|
model = (args.model_name or "").strip()
|
|
286
285
|
if config_path is not None and (not provider or not model):
|
|
@@ -10,9 +10,12 @@ import tomllib
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
12
|
import httpx
|
|
13
|
+
from monkeybot.core.config.settings import normalize_model_provider
|
|
13
14
|
from monkeybot.core.layout import AgentLayout, bootstrap_agent_layout
|
|
14
15
|
from monkeybot.core.memory.config import memory_enabled_from_config
|
|
15
16
|
from monkeybot.core.tools.sandbox_executor import SandboxConfig
|
|
17
|
+
from monkeybot.providers._bedrock_converse import uses_anthropic_bedrock
|
|
18
|
+
from monkeybot.providers.sampling import normalize_reasoning_effort
|
|
16
19
|
|
|
17
20
|
from monkeybot_cli.compat import COMPATIBLE_CORE_RANGE
|
|
18
21
|
from monkeybot_cli.config_resolve import (
|
|
@@ -228,6 +231,58 @@ def _strict_positive_int(raw: object) -> tuple[int | None, bool]:
|
|
|
228
231
|
return raw, False
|
|
229
232
|
|
|
230
233
|
|
|
234
|
+
def _add_reasoning_effort_check(
|
|
235
|
+
report: CommandReport,
|
|
236
|
+
*,
|
|
237
|
+
reasoning_effort: object,
|
|
238
|
+
provider: str = "",
|
|
239
|
+
model_name: str = "",
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Note a YAML effort value.
|
|
242
|
+
|
|
243
|
+
Severity stays ``warning``: the doctor schema has no ``info`` level, and a
|
|
244
|
+
passing check does not increment the warning summary. A non-Claude Bedrock
|
|
245
|
+
model fails this check because the Converse path does not send the field.
|
|
246
|
+
"""
|
|
247
|
+
text = normalize_reasoning_effort(reasoning_effort) or ""
|
|
248
|
+
converse = (
|
|
249
|
+
normalize_model_provider(provider) in {"aws_bedrock", "bedrock"}
|
|
250
|
+
and bool(model_name)
|
|
251
|
+
and not uses_anthropic_bedrock(model_name)
|
|
252
|
+
)
|
|
253
|
+
if text == "none":
|
|
254
|
+
message = "reasoning_effort turns thinking off where the provider supports it."
|
|
255
|
+
passed = True
|
|
256
|
+
elif text and converse:
|
|
257
|
+
message = (
|
|
258
|
+
f"reasoning_effort {text!r} is not sent for this Bedrock model. "
|
|
259
|
+
"Only Claude ids receive it; Converse models ignore it."
|
|
260
|
+
)
|
|
261
|
+
passed = False
|
|
262
|
+
elif text:
|
|
263
|
+
message = (
|
|
264
|
+
f"reasoning_effort {text!r} is sent to the provider. "
|
|
265
|
+
"It wins over thinking_budget where the provider accepts an effort level. "
|
|
266
|
+
"Gemini only sends levels its SDK knows; other values use thinking_budget. "
|
|
267
|
+
"A level the API rejects is omitted on later turns."
|
|
268
|
+
)
|
|
269
|
+
passed = True
|
|
270
|
+
else:
|
|
271
|
+
message = "reasoning_effort unset"
|
|
272
|
+
passed = True
|
|
273
|
+
check(
|
|
274
|
+
report,
|
|
275
|
+
id="model.reasoning_effort",
|
|
276
|
+
category="provider",
|
|
277
|
+
severity="warning",
|
|
278
|
+
passed=passed,
|
|
279
|
+
skip=not text,
|
|
280
|
+
message=message,
|
|
281
|
+
field="model.reasoning_effort",
|
|
282
|
+
value=text or None,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
231
286
|
def _add_ollama_local_checks(
|
|
232
287
|
report: CommandReport,
|
|
233
288
|
*,
|
|
@@ -464,11 +519,19 @@ def run_doctor(args: argparse.Namespace) -> int:
|
|
|
464
519
|
skip=True,
|
|
465
520
|
)
|
|
466
521
|
|
|
522
|
+
effort_raw = model.get("reasoning_effort") if isinstance(model, dict) else None
|
|
523
|
+
model_name = str(model.get("name", "")).strip() if isinstance(model, dict) else ""
|
|
524
|
+
_add_reasoning_effort_check(
|
|
525
|
+
report,
|
|
526
|
+
reasoning_effort=effort_raw,
|
|
527
|
+
provider=provider,
|
|
528
|
+
model_name=model_name,
|
|
529
|
+
)
|
|
530
|
+
|
|
467
531
|
thinking_raw = model.get("thinking_budget") if isinstance(model, dict) else None
|
|
468
532
|
if thinking_raw is None:
|
|
469
533
|
thinking_raw = "-1"
|
|
470
534
|
num_ctx_raw = model.get("num_ctx") if isinstance(model, dict) else None
|
|
471
|
-
model_name = str(model.get("name", "")).strip() if isinstance(model, dict) else ""
|
|
472
535
|
_add_ollama_local_checks(
|
|
473
536
|
report,
|
|
474
537
|
provider=provider,
|
|
@@ -9,7 +9,12 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
import httpx
|
|
11
11
|
|
|
12
|
-
from monkeybot_cli.config_resolve import
|
|
12
|
+
from monkeybot_cli.config_resolve import (
|
|
13
|
+
load_agent_dotenv,
|
|
14
|
+
load_config_doc,
|
|
15
|
+
resolve_agent_root,
|
|
16
|
+
resolve_config,
|
|
17
|
+
)
|
|
13
18
|
|
|
14
19
|
DEFAULT_GATEWAY_PORT = 8000
|
|
15
20
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""monkeybot mcp-login — browser sign-in for one remote MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from mcp.client.auth.exceptions import OAuthFlowError
|
|
14
|
+
from monkeybot.core.mcp.mcp_client import MCPAuthError
|
|
15
|
+
from monkeybot.core.mcp.oauth_flow import login_mcp_server
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
CLIENT_SECRET_ENV = "MONKEYBOT_MCP_CLIENT_SECRET"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run_mcp_login(args: argparse.Namespace) -> int:
|
|
23
|
+
"""Sign in, print JSON progress on stdout, and return 0 when the attempt finishes."""
|
|
24
|
+
client_secret = args.client_secret or os.environ.get(CLIENT_SECRET_ENV, "")
|
|
25
|
+
try:
|
|
26
|
+
result = asyncio.run(
|
|
27
|
+
login_mcp_server(
|
|
28
|
+
args.url,
|
|
29
|
+
Path(args.storage).expanduser(),
|
|
30
|
+
open_browser=not args.no_browser,
|
|
31
|
+
client_id=args.client_id,
|
|
32
|
+
client_secret=client_secret,
|
|
33
|
+
redirect_port=args.redirect_port,
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
except (MCPAuthError, OAuthFlowError) as exc:
|
|
37
|
+
print(str(exc), file=sys.stderr)
|
|
38
|
+
return 1
|
|
39
|
+
except Exception:
|
|
40
|
+
logger.exception("mcp oauth sign-in failed")
|
|
41
|
+
print("Sign-in failed.", file=sys.stderr)
|
|
42
|
+
return 1
|
|
43
|
+
print(json.dumps(result), flush=True)
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def register(sub: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
48
|
+
"""Register the ``mcp-login`` subcommand."""
|
|
49
|
+
command = sub.add_parser(
|
|
50
|
+
"mcp-login",
|
|
51
|
+
help="Sign in to a remote MCP server in the browser",
|
|
52
|
+
)
|
|
53
|
+
command.add_argument("--url", required=True, help="Streamable HTTP URL of the MCP server")
|
|
54
|
+
command.add_argument(
|
|
55
|
+
"--storage", required=True, help="Directory for this machine's OAuth tokens"
|
|
56
|
+
)
|
|
57
|
+
command.add_argument(
|
|
58
|
+
"--no-browser",
|
|
59
|
+
action="store_true",
|
|
60
|
+
help="Print the authorization URL instead of opening a browser",
|
|
61
|
+
)
|
|
62
|
+
command.add_argument(
|
|
63
|
+
"--client-id",
|
|
64
|
+
default="",
|
|
65
|
+
help="OAuth client id, for a server that will not register one automatically",
|
|
66
|
+
)
|
|
67
|
+
command.add_argument(
|
|
68
|
+
"--client-secret",
|
|
69
|
+
default="",
|
|
70
|
+
help=(
|
|
71
|
+
f"OAuth client secret, when that client requires one. Prefer {CLIENT_SECRET_ENV}: "
|
|
72
|
+
"command-line arguments are visible to other processes."
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
command.add_argument(
|
|
76
|
+
"--redirect-port",
|
|
77
|
+
type=int,
|
|
78
|
+
default=None,
|
|
79
|
+
help="Loopback port for the sign-in redirect, for a client registered with an exact URI",
|
|
80
|
+
)
|
|
81
|
+
command.set_defaults(func=run_mcp_login)
|
|
@@ -13,7 +13,7 @@ from monkeybot_cli.extras_catalog import (
|
|
|
13
13
|
additional_provider_extra_choices,
|
|
14
14
|
normalize_extra_token,
|
|
15
15
|
)
|
|
16
|
-
from monkeybot_cli.scaffold import run_new
|
|
16
|
+
from monkeybot_cli.scaffold import DEFAULT_TEMPLATE, TEMPLATES, run_new
|
|
17
17
|
|
|
18
18
|
_DEFAULT_PROVIDER = "gemini"
|
|
19
19
|
_DEFAULT_MODEL = "gemini-3-flash"
|
|
@@ -26,7 +26,19 @@ def _prompt_line(prompt: str) -> str:
|
|
|
26
26
|
return ""
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
TEMPLATE_CHOICES: tuple[ExtraChoice, ...] = (
|
|
30
|
+
ExtraChoice("default", "General-purpose agent"),
|
|
31
|
+
ExtraChoice("coding", "Coding agent for a repository (verification gate, dev toolchain)"),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _prompt_single(
|
|
36
|
+
choices: tuple[ExtraChoice, ...],
|
|
37
|
+
*,
|
|
38
|
+
title: str,
|
|
39
|
+
default_key: str,
|
|
40
|
+
noun: str = "provider",
|
|
41
|
+
) -> str:
|
|
30
42
|
"""Numbered single-select; Enter keeps default_key."""
|
|
31
43
|
print(title)
|
|
32
44
|
default_idx = 1
|
|
@@ -35,7 +47,7 @@ def _prompt_single(choices: tuple[ExtraChoice, ...], *, title: str, default_key:
|
|
|
35
47
|
if choice.key == default_key:
|
|
36
48
|
default_idx = i
|
|
37
49
|
print(f" {i}) {choice.key} — {choice.label}{marker}")
|
|
38
|
-
raw = _prompt_line(f"Choose
|
|
50
|
+
raw = _prompt_line(f"Choose {noun} [{default_idx}]: ").strip()
|
|
39
51
|
if not raw:
|
|
40
52
|
return default_key
|
|
41
53
|
if raw.isdigit():
|
|
@@ -48,7 +60,7 @@ def _prompt_single(choices: tuple[ExtraChoice, ...], *, title: str, default_key:
|
|
|
48
60
|
for choice in choices:
|
|
49
61
|
if raw.lower() == choice.key.lower():
|
|
50
62
|
return choice.key
|
|
51
|
-
print(f" unknown
|
|
63
|
+
print(f" unknown {noun} {raw!r}; using {default_key}", file=sys.stderr)
|
|
52
64
|
return default_key
|
|
53
65
|
|
|
54
66
|
|
|
@@ -128,6 +140,13 @@ def run_new_command(args: argparse.Namespace) -> int:
|
|
|
128
140
|
|
|
129
141
|
provider = args.provider
|
|
130
142
|
model = args.model
|
|
143
|
+
template = getattr(args, "template", None)
|
|
144
|
+
if template is not None and template not in TEMPLATES:
|
|
145
|
+
print(
|
|
146
|
+
f"error: unknown --template {template!r}; choose from {', '.join(TEMPLATES)}",
|
|
147
|
+
file=sys.stderr,
|
|
148
|
+
)
|
|
149
|
+
return 2
|
|
131
150
|
extras, unknown = _resolve_extras_from_args(getattr(args, "with_extras", None))
|
|
132
151
|
if unknown:
|
|
133
152
|
print(
|
|
@@ -137,6 +156,13 @@ def run_new_command(args: argparse.Namespace) -> int:
|
|
|
137
156
|
return 2
|
|
138
157
|
|
|
139
158
|
if not args.yes:
|
|
159
|
+
if template is None:
|
|
160
|
+
template = _prompt_single(
|
|
161
|
+
TEMPLATE_CHOICES,
|
|
162
|
+
title="Template:",
|
|
163
|
+
default_key=DEFAULT_TEMPLATE,
|
|
164
|
+
noun="template",
|
|
165
|
+
)
|
|
140
166
|
if provider is None:
|
|
141
167
|
provider = _prompt_single(
|
|
142
168
|
PROVIDER_CHOICES,
|
|
@@ -144,9 +170,7 @@ def run_new_command(args: argparse.Namespace) -> int:
|
|
|
144
170
|
default_key=_DEFAULT_PROVIDER,
|
|
145
171
|
)
|
|
146
172
|
if model is None:
|
|
147
|
-
model = (
|
|
148
|
-
_prompt_line(f"Model name [{_DEFAULT_MODEL}]: ").strip() or _DEFAULT_MODEL
|
|
149
|
-
)
|
|
173
|
+
model = _prompt_line(f"Model name [{_DEFAULT_MODEL}]: ").strip() or _DEFAULT_MODEL
|
|
150
174
|
# Only prompt for optional deps when --with was not already given.
|
|
151
175
|
if not extras:
|
|
152
176
|
more_providers = additional_provider_extra_choices(provider)
|
|
@@ -169,6 +193,7 @@ def run_new_command(args: argparse.Namespace) -> int:
|
|
|
169
193
|
provider=provider,
|
|
170
194
|
model=model,
|
|
171
195
|
extras=extras or None,
|
|
196
|
+
template=template or DEFAULT_TEMPLATE,
|
|
172
197
|
)
|
|
173
198
|
print(f"monkeybot scaffold under {dest}:")
|
|
174
199
|
print("\n".join(report))
|
|
@@ -179,6 +204,12 @@ def run_new_command(args: argparse.Namespace) -> int:
|
|
|
179
204
|
print(" cp .env.example .env # then add provider keys")
|
|
180
205
|
print(" monkeybot doctor")
|
|
181
206
|
print(" monkeybot chat")
|
|
207
|
+
if template == "coding":
|
|
208
|
+
print()
|
|
209
|
+
print("Coding template: clone the repository into ./workspace.")
|
|
210
|
+
print("Path prefixes in command_allowlist.yaml are relative to paths.workspace_root,")
|
|
211
|
+
print("so ../memory is the agent memory only while that stays ./workspace.")
|
|
212
|
+
print("If you set paths.workspace_root to another directory, update those prefixes.")
|
|
182
213
|
return 0
|
|
183
214
|
|
|
184
215
|
|
|
@@ -202,6 +233,13 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
202
233
|
"observability, openai, bedrock, claude"
|
|
203
234
|
),
|
|
204
235
|
)
|
|
236
|
+
p.add_argument(
|
|
237
|
+
"--template",
|
|
238
|
+
choices=TEMPLATES,
|
|
239
|
+
default=None,
|
|
240
|
+
help="Scaffold flavour: default (general agent) or coding (repo workflow). "
|
|
241
|
+
"Prompted interactively when omitted.",
|
|
242
|
+
)
|
|
205
243
|
p.add_argument("--force", action="store_true", help="Overwrite existing scaffold files")
|
|
206
244
|
p.add_argument("--yes", "-y", action="store_true", help="Skip interactive prompts")
|
|
207
245
|
p.set_defaults(func=run_new_command)
|
|
@@ -120,10 +120,19 @@ def run_validate(args: argparse.Namespace) -> int:
|
|
|
120
120
|
category="config",
|
|
121
121
|
severity="warning",
|
|
122
122
|
passed=not missing,
|
|
123
|
-
message=f"Missing includes: {', '.join(missing)}"
|
|
123
|
+
message=f"Missing includes: {', '.join(missing)}"
|
|
124
|
+
if missing
|
|
125
|
+
else "All includes resolved",
|
|
124
126
|
)
|
|
125
127
|
else:
|
|
126
|
-
check(
|
|
128
|
+
check(
|
|
129
|
+
report,
|
|
130
|
+
id="config.includes.resolve",
|
|
131
|
+
category="config",
|
|
132
|
+
severity="warning",
|
|
133
|
+
passed=True,
|
|
134
|
+
skip=True,
|
|
135
|
+
)
|
|
127
136
|
|
|
128
137
|
model = merged.get("model") if isinstance(merged.get("model"), dict) else {}
|
|
129
138
|
provider = str(model.get("provider", "")).strip().lower() if isinstance(model, dict) else ""
|
|
@@ -134,7 +143,9 @@ def run_validate(args: argparse.Namespace) -> int:
|
|
|
134
143
|
category="config",
|
|
135
144
|
severity="error",
|
|
136
145
|
passed=not provider or provider in SUPPORTED_YAML_MODEL_PROVIDERS,
|
|
137
|
-
message=f"model.provider '{provider}' is not supported"
|
|
146
|
+
message=f"model.provider '{provider}' is not supported"
|
|
147
|
+
if provider and provider not in SUPPORTED_YAML_MODEL_PROVIDERS
|
|
148
|
+
else "",
|
|
138
149
|
field="model.provider",
|
|
139
150
|
value=provider,
|
|
140
151
|
expected=sorted(SUPPORTED_YAML_MODEL_PROVIDERS),
|
|
@@ -172,12 +183,23 @@ def run_validate(args: argparse.Namespace) -> int:
|
|
|
172
183
|
)
|
|
173
184
|
|
|
174
185
|
flat = _flatten_to_env(merged)
|
|
175
|
-
flat.update(
|
|
186
|
+
flat.update(
|
|
187
|
+
{
|
|
188
|
+
k: v
|
|
189
|
+
for k, v in os.environ.items()
|
|
190
|
+
if k.startswith(("GCP_", "GOOGLE_", "ANTHROPIC_VERTEX"))
|
|
191
|
+
}
|
|
192
|
+
)
|
|
176
193
|
norm_provider = normalize_model_provider(provider or "gemini")
|
|
177
194
|
needs_gcp = norm_provider == "vertex_anthropic"
|
|
178
195
|
gcp_present = any(
|
|
179
196
|
os.environ.get(k, "").strip() or flat.get(k, "").strip()
|
|
180
|
-
for k in (
|
|
197
|
+
for k in (
|
|
198
|
+
"GCP_PROJECT_ID",
|
|
199
|
+
"VERTEX_AI_PROJECT_ID",
|
|
200
|
+
"ANTHROPIC_VERTEX_PROJECT_ID",
|
|
201
|
+
"GOOGLE_CLOUD_PROJECT",
|
|
202
|
+
)
|
|
181
203
|
)
|
|
182
204
|
if needs_gcp:
|
|
183
205
|
check(
|
|
@@ -190,11 +212,20 @@ def run_validate(args: argparse.Namespace) -> int:
|
|
|
190
212
|
remediation="Set gcp.project_id in monkeybot.yaml or GCP_PROJECT_ID in .env",
|
|
191
213
|
)
|
|
192
214
|
else:
|
|
193
|
-
check(
|
|
215
|
+
check(
|
|
216
|
+
report,
|
|
217
|
+
id="gcp.project.required",
|
|
218
|
+
category="config",
|
|
219
|
+
severity="error",
|
|
220
|
+
passed=True,
|
|
221
|
+
skip=True,
|
|
222
|
+
)
|
|
194
223
|
|
|
195
224
|
try:
|
|
196
225
|
validate_monkeybot_yaml_doc(merged, env=dict(os.environ))
|
|
197
|
-
validate_provider_env(
|
|
226
|
+
validate_provider_env(
|
|
227
|
+
{**flat, "MODEL_PROVIDER": provider or flat.get("MODEL_PROVIDER", "gemini")}
|
|
228
|
+
)
|
|
198
229
|
except ConfigError as exc:
|
|
199
230
|
check(
|
|
200
231
|
report,
|
|
@@ -329,7 +360,9 @@ def run_validate(args: argparse.Namespace) -> int:
|
|
|
329
360
|
category="mcp",
|
|
330
361
|
severity="warning",
|
|
331
362
|
passed=not missing_refs,
|
|
332
|
-
message=f"Unresolved MCP env refs: {', '.join(missing_refs)}"
|
|
363
|
+
message=f"Unresolved MCP env refs: {', '.join(missing_refs)}"
|
|
364
|
+
if missing_refs
|
|
365
|
+
else "All MCP env refs set",
|
|
333
366
|
)
|
|
334
367
|
if args.check_mcp:
|
|
335
368
|
for name, srv in servers.items():
|