monkeybot-cli 0.2.1__tar.gz → 0.5.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.
Files changed (94) hide show
  1. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/.gitignore +11 -4
  2. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/PKG-INFO +4 -3
  3. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/pyproject.toml +4 -2
  4. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/skills/monkeybot/SKILL.md +8 -5
  5. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/skills/monkeybot/references/config-sections.md +37 -18
  6. monkeybot_cli-0.5.0/src/monkeybot_cli/__init__.py +8 -0
  7. monkeybot_cli-0.5.0/src/monkeybot_cli/chat_file_index.py +101 -0
  8. monkeybot_cli-0.5.0/src/monkeybot_cli/chat_local_shell.py +178 -0
  9. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/chat_session.py +33 -6
  10. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/chat_status_bar.py +4 -2
  11. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/chat_tool_display.py +34 -128
  12. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/chat_tui.py +447 -21
  13. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/chat_tui_widgets.py +128 -7
  14. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/commands/chat.py +78 -6
  15. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/commands/doctor.py +175 -29
  16. monkeybot_cli-0.5.0/src/monkeybot_cli/commands/refresh.py +36 -0
  17. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/commands/run_cmd.py +28 -6
  18. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/commands/validate.py +17 -8
  19. monkeybot_cli-0.5.0/src/monkeybot_cli/compat.py +30 -0
  20. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/config_resolve.py +6 -12
  21. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/extras_catalog.py +2 -0
  22. monkeybot_cli-0.5.0/src/monkeybot_cli/gateway_health.py +71 -0
  23. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/main.py +5 -1
  24. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/opensandbox_lifecycle.py +25 -15
  25. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/providers.py +24 -1
  26. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/realtime/gateway_manager.py +5 -2
  27. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/realtime/session.py +11 -2
  28. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/realtime/session_controller.py +1 -1
  29. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/realtime/talk_ui.py +7 -2
  30. monkeybot_cli-0.5.0/src/monkeybot_cli/runtime_python.py +746 -0
  31. monkeybot_cli-0.5.0/src/monkeybot_cli/scaffold.py +435 -0
  32. monkeybot_cli-0.5.0/src/monkeybot_cli/scaffold_defaults/Dockerfile +27 -0
  33. monkeybot_cli-0.5.0/src/monkeybot_cli/scaffold_defaults/browser/SKILL.md +16 -0
  34. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/scaffold_defaults/command_allowlist.yaml +12 -2
  35. monkeybot_cli-0.5.0/src/monkeybot_cli/scaffold_defaults/dockerignore +5 -0
  36. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/scaffold_defaults/env.example +5 -0
  37. monkeybot_cli-0.5.0/src/monkeybot_cli/scaffold_defaults/image-generator/SKILL.md +72 -0
  38. monkeybot_cli-0.5.0/src/monkeybot_cli/scaffold_defaults/image-generator/generate_image.py +214 -0
  39. monkeybot_cli-0.5.0/src/monkeybot_cli/scaffold_defaults/loop/SKILL.md +45 -0
  40. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/scaffold_defaults/mcp.json +7 -6
  41. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/scaffold_defaults/monkeybot.example.yaml +91 -39
  42. monkeybot_cli-0.5.0/src/monkeybot_cli/scaffold_defaults/opensandbox.docker.toml +46 -0
  43. monkeybot_cli-0.5.0/tests/test_chat_continue.py +198 -0
  44. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/tests/test_chat_errors.py +1 -0
  45. monkeybot_cli-0.5.0/tests/test_chat_file_index.py +84 -0
  46. monkeybot_cli-0.5.0/tests/test_chat_local_shell.py +157 -0
  47. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/tests/test_chat_session.py +89 -0
  48. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/tests/test_chat_tui.py +1143 -3
  49. monkeybot_cli-0.5.0/tests/test_cli.py +319 -0
  50. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/tests/test_config_resolve.py +12 -1
  51. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/tests/test_doctor.py +37 -1
  52. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/tests/test_extras_catalog.py +16 -1
  53. monkeybot_cli-0.5.0/tests/test_gateway_health.py +163 -0
  54. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/tests/test_opensandbox_lifecycle.py +2 -2
  55. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/tests/test_realtime_session_controller.py +18 -0
  56. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/tests/test_run_cmd.py +15 -4
  57. monkeybot_cli-0.5.0/tests/test_runtime_python.py +1076 -0
  58. monkeybot_cli-0.5.0/tests/test_scaffold.py +220 -0
  59. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/uv.lock +287 -61
  60. monkeybot_cli-0.2.1/src/monkeybot_cli/__init__.py +0 -3
  61. monkeybot_cli-0.2.1/src/monkeybot_cli/compat.py +0 -7
  62. monkeybot_cli-0.2.1/src/monkeybot_cli/gateway_health.py +0 -34
  63. monkeybot_cli-0.2.1/src/monkeybot_cli/runtime_python.py +0 -91
  64. monkeybot_cli-0.2.1/src/monkeybot_cli/scaffold.py +0 -287
  65. monkeybot_cli-0.2.1/src/monkeybot_cli/scaffold_defaults/setup-workspace.sh +0 -24
  66. monkeybot_cli-0.2.1/tests/test_cli.py +0 -132
  67. monkeybot_cli-0.2.1/tests/test_runtime_python.py +0 -109
  68. monkeybot_cli-0.2.1/tests/test_scaffold.py +0 -115
  69. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/chat_renderer.py +0 -0
  70. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/chat_theme.py +0 -0
  71. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/commands/__init__.py +0 -0
  72. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/commands/loop.py +0 -0
  73. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/commands/new.py +0 -0
  74. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/commands/talk.py +0 -0
  75. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/exit_commands.py +0 -0
  76. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/output.py +0 -0
  77. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/realtime/__init__.py +0 -0
  78. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/realtime/audio_io.py +0 -0
  79. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/realtime/client.py +0 -0
  80. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/realtime/push_to_talk.py +0 -0
  81. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/realtime/wire_encode.py +0 -0
  82. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/scaffold_defaults/AGENT.md +0 -0
  83. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/scaffold_defaults/__init__.py +0 -0
  84. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/scaffold_defaults/otel-collector.example.yaml +0 -0
  85. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/scaffold_defaults/permissions.yaml +0 -0
  86. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/session_controller.py +0 -0
  87. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/src/monkeybot_cli/terminal_markdown.py +0 -0
  88. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/tests/test_chat_e2e.py +0 -0
  89. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/tests/test_chat_grounding.py +0 -0
  90. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/tests/test_chat_renderer_parity.py +0 -0
  91. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/tests/test_chat_status_bar.py +0 -0
  92. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/tests/test_chat_theme.py +0 -0
  93. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/tests/test_chat_tool_display.py +0 -0
  94. {monkeybot_cli-0.2.1 → monkeybot_cli-0.5.0}/tests/test_terminal_markdown.py +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
 
@@ -33,6 +30,12 @@ build/
33
30
 
34
31
  # Data (SQLite DB + memory files — never commit)
35
32
  data/
33
+ /memory/
34
+ .monkeybot/
35
+ *.sqlite3
36
+ workspace/.monkeybot/
37
+ *.sqlite
38
+ *.sqlite.writer-pid
36
39
 
37
40
  # Testing & coverage
38
41
  .pytest_cache/
@@ -40,6 +43,7 @@ data/
40
43
  htmlcov/
41
44
  .mypy_cache/
42
45
  .ruff_cache/
46
+ .deepeval/
43
47
 
44
48
  # macOS
45
49
  .DS_Store
@@ -61,4 +65,7 @@ docker/data/
61
65
  code/
62
66
 
63
67
  # Local venv for evals dependency experiments
64
- .evals-venv/
68
+ .evals-venv/
69
+
70
+ # monkeythink
71
+ .monkeythink/
@@ -1,10 +1,11 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: monkeybot-cli
3
- Version: 0.2.1
3
+ Version: 0.5.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[cli]<3,>=2.1.0
7
+ Requires-Dist: monkeybot-browser-mcp<1,>=0.2.0
8
+ Requires-Dist: monkeybot[cli]<4,>=3.0.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.2.1"
7
+ version = "0.5.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.1.0,<3",
13
+ "monkeybot[cli]>=3.0.0,<4",
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/`, `workspace/` (file-tool sandbox), `workspace/skills` → `skills/`, `data/memory/`, `.env.example`, `scripts/setup-workspace.sh`, and an agent `pyproject.toml`. Use `--force` only when overwriting is explicitly requested.
88
+ Creates `monkeybot_config/`, read-only `skills/`, writable `workspace/`,
89
+ `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
 
@@ -111,7 +113,7 @@ uv sync
111
113
  | `huggingface` | `HF_TOKEN` (or `HUGGINGFACE_API_KEY`) | `monkeybot[huggingface]` |
112
114
  | `ollama` | None required — `OLLAMA_BASE_URL` (default `http://localhost:11434`) for a non-default server | `monkeybot[ollama]` |
113
115
 
114
- **Agent-first dependencies.** The CLI is thin — it does **not** install provider/storage extras globally. `monkeybot new` scaffolds a `pyproject.toml` with the selected provider (and any `--with` extras). Run plain `uv sync` in the agent directory. `monkeybot run` / `chat` spawn the gateway from that project's interpreter (`.venv/bin/python`, else `uv run python`), and `doctor` checks extras in that same interpreter. For a config-only tree (just `monkeybot_config/`, no `pyproject.toml`) the gateway falls back to the CLI's interpreter, so extras must be installed in the CLI env (`uv tool install --with 'monkeybot[<extra>]' monkeybot-cli`).
116
+ **Agent-first dependencies.** The CLI is thin — it does **not** install provider/storage extras globally. `monkeybot new` scaffolds a `pyproject.toml` with the selected provider (and any `--with` extras). Run plain `uv sync` in the agent directory. `monkeybot run` / `chat` spawn the gateway from that project's interpreter (`.venv/bin/python`, else `uv run python`), and `doctor` checks extras in that same interpreter. For a config-only tree (just `monkeybot_config/`, no `pyproject.toml`) the gateway uses the CLI's interpreter when it already has MonkeyBot 3.x (and MemPalace if memory is on). If memory is enabled and that interpreter cannot import MemPalace, `run` / `chat` provision a cached CLI-managed venv under `~/.cache/monkeybot/runtimes/` holding `monkeybot[memory]` pinned to the running core (never rewrites a `pyproject.toml`). `doctor` reuses that cache when it is already present. Otherwise extras must be installed in the CLI env (`uv tool install --with 'monkeybot[<extra>]' monkeybot-cli`).
115
117
 
116
118
  `doctor` is the source of truth for credentials and extras — when in doubt, run it and read the `remediation` field (add `monkeybot[<extra>]` to agent deps + `uv sync`).
117
119
 
@@ -154,13 +156,13 @@ Decision → config map:
154
156
  | "I have a custom web UI" | `gateway.cors_allow_origins` |
155
157
  | "Search the web" | `web_search.backend` + `.env` keys (Tavily/Firecrawl) |
156
158
  | "Run untrusted code" | `sandbox.enabled` + `SANDBOX_API_KEY` |
157
- | "Use specialist agents" | `subagents[]` + `monkeybot_config/agents/*.md` |
159
+ | "Use specialist agents" | `subagents.personas` + `monkeybot_config/agents/*.md` |
158
160
  | "Connect external tools" | `mcp.json` (`mcpServers` object), then `validate --check-mcp` |
159
161
  | "Control cost / context size" | `model.*`, `context_curation.*` |
160
162
  | "Restrict dangerous commands" | `tools.denied_patterns`, `command_allowlist.yaml` |
161
163
  | "Multiple environments" | top-level `includes:` fragments |
162
164
 
163
- **Subagents (`task` tool):** share the parent `AGENT.md` (or `subagent.agent_md`). Relative paths resolve from the bot project root, not `workspace/`. Specialize via `task` / `context`, not separate agent type folders. For parallel `task` fan-out, prefer Postgres: add `monkeybot[postgres]` to the **agent** `pyproject.toml` dependencies, run `uv sync`, then set `DB_URL=postgresql://...` in `.env`.
165
+ **Subagents (`task` tool):** without `subagent_type`, share the parent `AGENT.md`. With a persona, use that persona's `agent_md`. Relative paths resolve from the bot project root, not `workspace/`. Specialize via `task` / `context` or named personas. For parallel `task` fan-out, prefer Postgres: add `monkeybot[postgres]` to the **agent** `pyproject.toml` dependencies, run `uv sync`, then set `DB_URL=postgresql://...` in `.env`.
164
166
 
165
167
  **Observability** is mostly env + add `monkeybot[observability]` to agent deps + `uv sync` + an OTel collector — not `monkeybot.yaml`. See `docs/observability-runbook.md`.
166
168
 
@@ -189,7 +191,8 @@ uv tool install --editable .
189
191
 
190
192
  | Command | Purpose |
191
193
  |---------|---------|
192
- | `new` | Scaffold `monkeybot_config/`, `workspace/`, `data/memory/`, `skills/`, `pyproject.toml`, `.env.example` |
194
+ | `new` | Scaffold `monkeybot_config/`, `workspace/`, `memory/`, `skills/`, `pyproject.toml`, `.env.example` |
195
+ | `refresh` | Additive update of packaged YAML defaults on an existing agent (keeps AGENT.md, mcp.json, model) |
193
196
  | `validate` | Config + paths + MCP shape (`--check-mcp` for network) |
194
197
  | `doctor` | Python, provider extra, credentials, port |
195
198
  | `run` | Start SSE gateway subprocess |
@@ -21,10 +21,11 @@ Validate/doctor: `doctor` → `runtime.port.free`.
21
21
  | Field | Default | When to change |
22
22
  |---|---|---|
23
23
  | `agent_md` | `./monkeybot_config/AGENT.md` | Alternate system-prompt location |
24
- | `memory_storage_uri` | `local://./data/memory` | `gcs://…` for shared/cloud memory (requires GCP project) |
24
+ | `memory_storage_uri` | `local://./memory` | `gcs://…` for shared/cloud memory (requires GCP project) |
25
25
  | `skills_path` | `./skills` | Point at a different skills tree |
26
26
  | `db_url` | `sqlite:///data/monkeybot.db` | **Postgres for parallel subagents** — SQLite hits `database is locked` under concurrency |
27
27
  | `auto_schema` | `true` | Set `false` when migrations own the schema (managed Postgres with DML-only runtime user) |
28
+ | `agent_id` | resolved agent root path | **Set explicitly when this agent is relocatable** — moved to a new path, redeployed with a different mount point, or run as multiple replicas that must share one conversation history. Without it, conversation-history scoping (which agent owns which thread, on a shared `db_url`) keys off the filesystem path, so a path change strands existing history. Once set, keep the value stable across deploys. |
28
29
  | `mcp_config` | `./monkeybot_config/mcp.json` | Relocate MCP definitions |
29
30
  | `command_allowlist_config` | `./monkeybot_config/command_allowlist.yaml` | Relocate the shell allowlist |
30
31
  | `workspace_root` | `./workspace` (if present) | Change the file-tool sandbox root |
@@ -40,8 +41,8 @@ Validate check ids: `paths.agent_md.exists`, `paths.skills_path.exists`, `paths.
40
41
  | `temperature` | `0.7` | Lower for deterministic output, higher for creative |
41
42
  | `max_tokens` | `60000` | Cap per-response length |
42
43
  | `thinking_budget` | `-1` | Gemini: `-1` model default, `0` off, `N` token budget. Ollama reasoning models: `-1` server default, `0` off (`reasoning_effort: none`) |
43
- | `context_window` | `1000000` | Summarization trigger threshold (tokens) |
44
- | `max_turns` | `50` | Hard cap on turns per run |
44
+ | `context_window` | `1000000` | Summarization trigger (tokens); also drives soft-spill / `read_file` char budgets |
45
+ | `max_turns` | `1000` | Hard cap on turns per run |
45
46
  | `summarization_model` | (main model) | Cheaper model for history summarization (env `CONTEXT_SUMMARIZATION_MODEL`) |
46
47
 
47
48
  Validate check ids: `model.provider.supported`, `model.name.present`. Supported YAML providers: `gemini`/`vertex`, `openai`, `anthropic`, `vertex-claude`, `huggingface`, `ollama`, `aws_bedrock`, `fake`.
@@ -86,40 +87,48 @@ Recent window by default; LLM curator only when the index is token-heavy. On cur
86
87
 
87
88
  When the prompt shows fewer entries than exist, a structural confidence score triggers a `search_memory` nudge. Skill names are always shown in full in the prompt; use `list_skills` to get the skills root path.
88
89
 
89
- ## `memory_hook`
90
+ ## `memory`
90
91
 
91
- `enabled: true` automatic memory capture after turns. Disable to manage memory manually.
92
+ MemPalace capture, wake-up, and search teaching are on by default. Turn them off with `memory.enabled: false` or `MONKEYBOT_MEMORY_HOOK_ENABLED=0`. The vector stack is the optional `monkeybot[memory]` extra omit it (and set `enabled: false`) when you do not want chromadb/onnxruntime.
92
93
 
93
- ## `subagent` and `subagents`
94
+ | Field | Default | When to change |
95
+ |---|---|---|
96
+ | `enabled` | `true` | Set `false` to skip capture, wake-up, and prompt teaching |
97
+ | `backend` | `chroma` | Alternate MemPalace vector backend |
98
+ | `embedding_model` | `embeddinggemma-300m` | Match the embedder the palace was built with |
99
+
100
+ ## `subagents`
94
101
 
95
- `subagent` sets defaults for `task` calls:
102
+ Global defaults for `task` calls, plus optional named personas:
96
103
 
97
104
  | Field | Default | Notes |
98
105
  |---|---|---|
99
106
  | `timeout_sec` | `600` | Per-subagent timeout |
100
- | `max_turns` | `25` | Per-subagent turn cap |
107
+ | `max_turns` | `1000` | Per-subagent turn cap |
101
108
  | `vertex_google_search` | `false` | **Gemini only.** Enables native `google_search` grounding for subagent `task` runs. Config-file only. |
102
- | `agent_md` | (parent `AGENT.md`) | Default prompt when `task` omits `subagent_type` |
103
-
104
- `subagents[]` defines named personas the parent selects via `task(subagent_type=...)`:
109
+ | `personas` | (none) | Named types selected via `task(subagent_type=...)`; each persona sets its own `agent_md` |
105
110
 
106
111
  ```yaml
107
112
  subagents:
108
- - name: researcher
109
- description: "Deep-dives a topic and returns a structured summary."
110
- agent_md: ./monkeybot_config/agents/researcher.md
113
+ timeout_sec: 600
114
+ max_turns: 1000
115
+ vertex_google_search: false
116
+ personas:
117
+ - name: researcher
118
+ description: "Deep-dives a topic and returns a structured summary."
119
+ agent_md: ./monkeybot_config/agents/researcher.md
111
120
  ```
112
121
 
113
- Relative paths resolve from the bot project root, not `workspace/`. For parallel fan-out, use Postgres (`db_url`).
122
+ Without a `subagent_type`, the task inherits the parent `AGENT.md` (`paths.agent_md`). Relative paths resolve from the bot project root, not `workspace/`. For parallel fan-out, use Postgres (`db_url`).
114
123
 
115
124
  ## `tools`
116
125
 
117
126
  | Field | Default | When to change |
118
127
  |---|---|---|
119
128
  | `denied_patterns` | (none) | Block substrings in tool args, e.g. `"rm -rf"` (also env `MONKEYBOT_TOOL_DENIED_PATTERNS`) |
120
- | `read_max_lines` / `read_default_lines` | (code defaults) | Tune file-read limits |
121
- | `spill_read_max_lines` / `spill_min_chars` | (code defaults) | Tune large-result spill behavior |
122
- | `result_budget_fraction` / `result_budget_floor_tokens` | (code defaults) | Advanced result-budgeting; rarely needed |
129
+ | `read_max_lines` | `5000` | Cap on `read_file` `limit` (**YAML only** — no env override). Default when `limit` is omitted is harness-fixed at **2000** (pass `limit` to request more). |
130
+
131
+ Spill and `read_file` char budgets are derived from `model.context_window` (retired keys `spill_min_chars` / `spill_read_max_lines` / `read_default_lines` warn and are ignored). Context pressure ratios and tool-result budget fractions are fixed in harness code (not YAML/env).
123
132
 
124
133
  For shell-command safety, pair `denied_patterns` with `monkeybot_config/command_allowlist.yaml`.
125
134
 
@@ -144,6 +153,16 @@ Tavily/Firecrawl need `TAVILY_API_KEY` / `FIRECRAWL_API_KEY` in `.env`. Doctor c
144
153
 
145
154
  Needs `SANDBOX_API_KEY` in `.env`.
146
155
 
156
+ ## `scheduler`
157
+
158
+ Prompt-first scheduled loops (`start_loop`, `/scheduler/loops`). Requires durable storage (`paths.db_url`). Loops are registered at runtime — there is no static `jobs` list in YAML.
159
+
160
+ | Field | Default | When to change |
161
+ |---|---|---|
162
+ | `enabled` | `false` | `true` runs the tick worker in-process on the gateway (local/dev). Production: leave `false` and run `python -m monkeybot.scheduler` as a separate process |
163
+
164
+ Env override: `MONKEYBOT_SCHEDULER_ENABLED` (`1` \| `true` \| `yes` \| `on`).
165
+
147
166
  ## `includes`
148
167
 
149
168
  Top-level list of YAML fragments (paths relative to the config file's directory). Later files deep-merge over earlier ones — useful for per-environment overrides:
@@ -0,0 +1,8 @@
1
+ """monkeybot setup CLI — create, configure, validate, and chat with agents."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("monkeybot-cli")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0-dev"
@@ -0,0 +1,101 @@
1
+ """Pure helpers for the ``@`` file-mention palette in ``monkeybot chat``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import subprocess
8
+ from pathlib import Path
9
+
10
+ _AT_TOKEN_RE = re.compile(r"@(\S*)$")
11
+
12
+ _SKIP_DIRS = frozenset(
13
+ {
14
+ ".git",
15
+ ".venv",
16
+ "venv",
17
+ "node_modules",
18
+ "__pycache__",
19
+ ".mypy_cache",
20
+ ".pytest_cache",
21
+ ".ruff_cache",
22
+ "data",
23
+ }
24
+ )
25
+
26
+
27
+ def detect_at_token(line: str, col: int) -> tuple[int, str] | None:
28
+ """Return ``(start_col, query)`` if the cursor sits in an ``@token``.
29
+
30
+ Only triggers when the ``@`` starts the line or follows whitespace, so
31
+ mid-word ``@`` (e.g. ``a@b.com``) never matches.
32
+ """
33
+ prefix = line[:col]
34
+ match = _AT_TOKEN_RE.search(prefix)
35
+ if match is None:
36
+ return None
37
+ start = match.start()
38
+ if start > 0 and not prefix[start - 1].isspace():
39
+ return None
40
+ return start, match.group(1)
41
+
42
+
43
+ def list_workspace_files(root: Path, *, limit: int = 3000) -> list[str]:
44
+ """List files under ``root``, preferring git (honors .gitignore)."""
45
+ try:
46
+ result = subprocess.run(
47
+ ["git", "-C", str(root), "ls-files", "--cached", "--others", "--exclude-standard"],
48
+ capture_output=True,
49
+ text=True,
50
+ timeout=5.0,
51
+ check=False,
52
+ )
53
+ if result.returncode == 0:
54
+ files = [line for line in result.stdout.splitlines() if line.strip()]
55
+ if files:
56
+ return sorted(files)[:limit]
57
+ except (OSError, subprocess.SubprocessError):
58
+ pass
59
+ return _walk_files(root, limit=limit)
60
+
61
+
62
+ def _walk_files(root: Path, *, limit: int) -> list[str]:
63
+ out: list[str] = []
64
+ for dirpath, dirnames, filenames in os.walk(root):
65
+ dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS and not d.startswith(".")]
66
+ rel_dir = os.path.relpath(dirpath, root)
67
+ for name in filenames:
68
+ rel = name if rel_dir == "." else f"{rel_dir}/{name}"
69
+ out.append(rel.replace(os.sep, "/"))
70
+ if len(out) >= limit:
71
+ return sorted(out)
72
+ return sorted(out)
73
+
74
+
75
+ def fuzzy_filter_files(files: list[str], query: str, *, limit: int = 8) -> list[str]:
76
+ """Case-insensitive subsequence match, ranked basename-prefix > path-prefix > contiguity."""
77
+ if not query:
78
+ return files[:limit]
79
+ q = query.lower()
80
+ scored: list[tuple[int, int, str]] = []
81
+ for path in files:
82
+ low = path.lower()
83
+ basename = low.rsplit("/", 1)[-1]
84
+ if not _is_subsequence(q, low):
85
+ continue
86
+ if basename.startswith(q):
87
+ rank = 0
88
+ elif low.startswith(q):
89
+ rank = 1
90
+ elif q in low:
91
+ rank = 2
92
+ else:
93
+ rank = 3
94
+ scored.append((rank, len(path), path))
95
+ scored.sort()
96
+ return [path for _, _, path in scored[:limit]]
97
+
98
+
99
+ def _is_subsequence(query: str, text: str) -> bool:
100
+ it = iter(text)
101
+ return all(ch in it for ch in query)
@@ -0,0 +1,178 @@
1
+ """Run a local shell command from the ``!`` prefix in ``monkeybot chat``.
2
+
3
+ Output is local-only — never sent to the agent.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import signal
10
+ import subprocess
11
+ import sys
12
+ import threading
13
+ from pathlib import Path
14
+
15
+ _IS_WINDOWS = sys.platform == "win32"
16
+
17
+ # Cap capture before TUI truncation so `!yes`-style floods cannot OOM the process.
18
+ _MAX_CAPTURE_CHARS = 100_000
19
+ _READ_CHUNK = 4_096
20
+
21
+
22
+ def _popen_kwargs_for_platform() -> dict[str, object]:
23
+ if _IS_WINDOWS:
24
+ # getattr fallback: this constant only exists on Windows builds of
25
+ # `subprocess`, so a plain attribute access breaks importing/testing
26
+ # this module on POSIX even though the branch never runs there.
27
+ return {"creationflags": getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)}
28
+ return {"start_new_session": True}
29
+
30
+
31
+ def _kill_process_tree(pid: int) -> None:
32
+ """Best-effort kill of the shell process group/tree.
33
+
34
+ Swallow lookup/permission races — leader exit can make killpg raise
35
+ ``PermissionError`` even though descendants still need a signal.
36
+ """
37
+ if _IS_WINDOWS:
38
+ try:
39
+ subprocess.run(
40
+ ["taskkill", "/F", "/T", "/PID", str(pid)],
41
+ capture_output=True,
42
+ check=False,
43
+ )
44
+ except OSError:
45
+ pass
46
+ return
47
+ try:
48
+ os.killpg(pid, signal.SIGKILL)
49
+ except (ProcessLookupError, PermissionError, OSError):
50
+ try:
51
+ os.kill(pid, signal.SIGKILL)
52
+ except (ProcessLookupError, PermissionError, OSError):
53
+ pass
54
+
55
+
56
+ def _append_capped(chunks: list[str], total: int, data: str) -> tuple[int, bool]:
57
+ """Append ``data`` up to the capture cap. Returns (new_total, hit_cap)."""
58
+ remain = _MAX_CAPTURE_CHARS - total
59
+ if remain <= 0:
60
+ return total, True
61
+ if len(data) <= remain:
62
+ chunks.append(data)
63
+ return total + len(data), False
64
+ chunks.append(data[:remain])
65
+ return total + remain, True
66
+
67
+
68
+ def run_local_shell(command: str, cwd: Path, *, timeout: float = 120.0) -> tuple[str, int | None]:
69
+ """Run ``command`` in a shell, returning merged stdout+stderr and the exit code.
70
+
71
+ Runs as the root of its own process group/tree so a timeout can kill
72
+ everything it spawned — plain ``subprocess.run(..., timeout=...)`` only
73
+ kills the shell itself, leaving children it spawned (e.g. a backgrounded
74
+ ``sleep``) running past the reported timeout.
75
+
76
+ Output is capped at ``_MAX_CAPTURE_CHARS`` while streaming (never fully
77
+ buffered first), so unbounded writers cannot exhaust memory before the
78
+ TUI truncates for display. Works the same on POSIX and Windows.
79
+ """
80
+ proc = subprocess.Popen(
81
+ command,
82
+ shell=True,
83
+ cwd=cwd,
84
+ stdout=subprocess.PIPE,
85
+ stderr=subprocess.STDOUT,
86
+ text=True,
87
+ **_popen_kwargs_for_platform(),
88
+ )
89
+ assert proc.stdout is not None
90
+
91
+ chunks: list[str] = []
92
+ state = {"total": 0, "capped": False}
93
+ stop_reader = threading.Event()
94
+
95
+ def _reader() -> None:
96
+ stdout = proc.stdout
97
+ if stdout is None:
98
+ return
99
+ try:
100
+ while not stop_reader.is_set():
101
+ chunk = stdout.read(_READ_CHUNK)
102
+ if chunk == "":
103
+ break
104
+ state["total"], hit = _append_capped(chunks, state["total"], chunk)
105
+ if hit:
106
+ state["capped"] = True
107
+ _kill_process_tree(proc.pid)
108
+ break
109
+ except (ValueError, OSError):
110
+ # stdout closed from the main thread to unblock a stuck read.
111
+ return
112
+
113
+ reader = threading.Thread(target=_reader, name="monkeybot-local-shell-reader", daemon=True)
114
+ reader.start()
115
+
116
+ timed_out = False
117
+ try:
118
+ try:
119
+ proc.wait(timeout=timeout)
120
+ except subprocess.TimeoutExpired:
121
+ timed_out = True
122
+ _kill_process_tree(proc.pid)
123
+
124
+ # Shell may have exited while a descendant still holds the pipe —
125
+ # kill the tree and close stdout so the reader cannot block forever.
126
+ if reader.is_alive():
127
+ _kill_process_tree(proc.pid)
128
+ stop_reader.set()
129
+ try:
130
+ proc.stdout.close()
131
+ except OSError:
132
+ pass
133
+ reader.join(timeout=2.0)
134
+ if reader.is_alive() and not timed_out:
135
+ # Still stuck after kill+close: treat as timeout so the TUI
136
+ # always gets a bounded result instead of hanging.
137
+ timed_out = True
138
+ else:
139
+ reader.join(timeout=0.1)
140
+ except Exception:
141
+ timed_out = True
142
+ _kill_process_tree(proc.pid)
143
+ stop_reader.set()
144
+ try:
145
+ proc.stdout.close()
146
+ except OSError:
147
+ pass
148
+ reader.join(timeout=1.0)
149
+
150
+ try:
151
+ if proc.poll() is None:
152
+ _kill_process_tree(proc.pid)
153
+ try:
154
+ proc.wait(timeout=1)
155
+ except Exception:
156
+ pass
157
+ except Exception:
158
+ pass
159
+
160
+ output = "".join(chunks)
161
+ if timed_out:
162
+ # ``None`` means timed out (distinct from a real exit status).
163
+ return output + f"\n(timed out after {timeout:.0f}s)", None
164
+ if state["capped"]:
165
+ output += f"\n… (output capped at {_MAX_CAPTURE_CHARS} chars)"
166
+ return output, proc.returncode
167
+
168
+
169
+ def truncate_output(text: str, *, max_lines: int = 200, max_chars: int = 10_000) -> str:
170
+ """Bound output for transcript display, noting how much was dropped."""
171
+ lines = text.splitlines()
172
+ if len(lines) > max_lines:
173
+ dropped = len(lines) - max_lines
174
+ text = "\n".join(lines[:max_lines]) + f"\n… (+{dropped} lines truncated)"
175
+ if len(text) > max_chars:
176
+ dropped_chars = len(text) - max_chars
177
+ text = text[:max_chars] + f"\n… (+{dropped_chars} chars truncated)"
178
+ return text
@@ -19,6 +19,7 @@ from monkeybot.core.runtime.events import (
19
19
  AssistantDelta,
20
20
  ContextSummarized,
21
21
  ContextSummarizing,
22
+ ContextUsage,
22
23
  Error,
23
24
  FrontendToolRequestEvent,
24
25
  GroundingEvent,
@@ -228,6 +229,7 @@ class ChatSessionController:
228
229
  self.usage = UsageStore()
229
230
  self.session_id: str | None = None
230
231
  self.stream_error = False
232
+ self.transcript_report_dir: str | None = None
231
233
  self._client: httpx.AsyncClient | None = None
232
234
  self._event_queue: asyncio.Queue[str | None] = asyncio.Queue()
233
235
  self._stream_task: asyncio.Task[None] | None = None
@@ -617,11 +619,21 @@ class ChatSessionController:
617
619
  )
618
620
  return
619
621
  if isinstance(evt, ContextSummarizing):
620
- self._on_context_summarizing(evt)
622
+ self._on_context_usage_hint(
623
+ estimated_tokens=evt.estimated_tokens,
624
+ context_window_tokens=evt.context_window_tokens,
625
+ )
626
+ self._emit("summarizing", tokens=evt.estimated_tokens)
627
+ return
628
+ if isinstance(evt, ContextUsage):
629
+ self._on_context_usage_hint(
630
+ estimated_tokens=evt.estimated_tokens,
631
+ context_window_tokens=evt.context_window_tokens,
632
+ )
621
633
  return
622
634
  if isinstance(evt, ContextSummarized):
635
+ # Mid-turn /usage is stale; post-compaction ContextUsage updates the ring.
623
636
  self._emit("summarized", turns=evt.turns_summarized)
624
- await self._fetch_usage()
625
637
  return
626
638
  if isinstance(evt, AssistantDelta):
627
639
  self._on_assistant_delta(evt, state)
@@ -644,13 +656,14 @@ class ChatSessionController:
644
656
 
645
657
  self._maybe_thinking_trace(evt, request_id)
646
658
 
647
- def _on_context_summarizing(self, evt: ContextSummarizing) -> None:
659
+ def _on_context_usage_hint(
660
+ self, *, estimated_tokens: int, context_window_tokens: int
661
+ ) -> None:
648
662
  self.usage.update_context_hint(
649
- estimated_prompt_tokens=evt.estimated_tokens,
650
- context_window_tokens=evt.context_window_tokens,
663
+ estimated_prompt_tokens=estimated_tokens,
664
+ context_window_tokens=context_window_tokens,
651
665
  )
652
666
  self._emit("usage_updated", usage=self.usage.usage)
653
- self._emit("summarizing", tokens=evt.estimated_tokens)
654
667
 
655
668
  def _on_assistant_delta(self, evt: AssistantDelta, state: _TurnState) -> None:
656
669
  if not state.assistant_started:
@@ -906,6 +919,20 @@ class ChatSessionController:
906
919
  await asyncio.wait_for(self._stream_task, timeout=0.5)
907
920
  for task in list(self._cancel_tasks):
908
921
  task.cancel()
922
+ if self._client is not None and self.session_id is not None:
923
+ try:
924
+ resp = await self._client.delete(f"{self.base}/sessions/{self.session_id}")
925
+ if resp.status_code == 200:
926
+ data = resp.json()
927
+ report = data.get("transcript_report_dir") if isinstance(data, dict) else None
928
+ if isinstance(report, str) and report.strip():
929
+ self.transcript_report_dir = report.strip()
930
+ except Exception:
931
+ logger.warning(
932
+ "session DELETE on close failed session_id=%s",
933
+ self.session_id,
934
+ exc_info=True,
935
+ )
909
936
  if self._client is not None:
910
937
  await self._client.aclose()
911
938
  self._client = None
@@ -4,6 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass
6
6
 
7
+ from monkeybot.core.runtime.context_budget import SUMMARY_TRIGGER_RATIO
8
+
7
9
  _DIM = "\x1b[2m"
8
10
  _GREEN = "\x1b[32m"
9
11
  _YELLOW = "\x1b[33m"
@@ -37,7 +39,7 @@ def parse_usage_response(data: dict[str, object]) -> SessionUsageView:
37
39
  cap = _int("context_window_tokens", DEFAULT_CONTEXT_WINDOW)
38
40
  thresh = _int("summarization_threshold_tokens", 0)
39
41
  if thresh <= 0:
40
- thresh = max(1, int(cap * 0.85))
42
+ thresh = max(1, int(cap * SUMMARY_TRIGGER_RATIO))
41
43
  return SessionUsageView(
42
44
  input_tokens=_int("input_tokens"),
43
45
  output_tokens=_int("output_tokens"),
@@ -85,7 +87,7 @@ def _ring_parts(
85
87
  1,
86
88
  summarization_threshold_tokens
87
89
  if summarization_threshold_tokens > 0
88
- else int(cap * 0.85),
90
+ else int(cap * SUMMARY_TRIGGER_RATIO),
89
91
  )
90
92
  pct_used = min(100, max(0, round((ring_numerator / cap) * 100)))
91
93
  return _ring_glyph(pct_used), pct_used, ring_numerator, thresh