monkeybot-cli 0.10.0__tar.gz → 0.11.1__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 (96) hide show
  1. monkeybot_cli-0.11.1/.gitignore +90 -0
  2. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/PKG-INFO +2 -2
  3. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/pyproject.toml +8 -2
  4. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/skills/monkeybot/SKILL.md +1 -1
  5. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/skills/monkeybot/references/config-sections.md +3 -1
  6. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/chat_tui_widgets.py +58 -1
  7. monkeybot_cli-0.11.1/src/monkeybot_cli/commands/mcp_login.py +81 -0
  8. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/commands/new.py +44 -4
  9. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/main.py +13 -1
  10. monkeybot_cli-0.11.1/src/monkeybot_cli/overrides.txt +5 -0
  11. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/runtime_python.py +161 -50
  12. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/scaffold.py +88 -16
  13. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/scaffold_defaults/command_allowlist.yaml +2 -0
  14. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/scaffold_defaults/monkeybot.example.yaml +16 -1
  15. monkeybot_cli-0.11.1/src/monkeybot_cli/scaffold_defaults/templates/coding/AGENT.md +35 -0
  16. monkeybot_cli-0.11.1/src/monkeybot_cli/scaffold_defaults/templates/coding/command_allowlist.yaml +92 -0
  17. monkeybot_cli-0.11.1/src/monkeybot_cli/scaffold_defaults/templates/coding/monkeybot.overrides.yaml +31 -0
  18. monkeybot_cli-0.11.1/src/monkeybot_cli/scaffold_defaults/templates/coding/permissions.yaml +42 -0
  19. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_chat_tui.py +79 -0
  20. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_cli.py +44 -0
  21. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_runtime_python.py +170 -15
  22. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_scaffold.py +194 -0
  23. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/uv.lock +200 -179
  24. monkeybot_cli-0.10.0/.gitignore +0 -71
  25. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/__init__.py +0 -0
  26. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/chat_file_index.py +0 -0
  27. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/chat_local_shell.py +0 -0
  28. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/chat_renderer.py +0 -0
  29. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/chat_session.py +0 -0
  30. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/chat_status_bar.py +0 -0
  31. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/chat_theme.py +0 -0
  32. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/chat_tool_display.py +0 -0
  33. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/chat_tui.py +0 -0
  34. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/commands/__init__.py +0 -0
  35. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/commands/chat.py +0 -0
  36. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/commands/doctor.py +0 -0
  37. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/commands/loop.py +0 -0
  38. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/commands/refresh.py +0 -0
  39. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/commands/run_cmd.py +0 -0
  40. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/commands/talk.py +0 -0
  41. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/commands/trace.py +0 -0
  42. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/commands/validate.py +0 -0
  43. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/compat.py +0 -0
  44. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/config_resolve.py +0 -0
  45. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/exit_commands.py +0 -0
  46. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/extras_catalog.py +0 -0
  47. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/gateway_health.py +0 -0
  48. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/opensandbox_lifecycle.py +0 -0
  49. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/output.py +0 -0
  50. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/process_tree.py +0 -0
  51. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/providers.py +0 -0
  52. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/realtime/__init__.py +0 -0
  53. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/realtime/audio_io.py +0 -0
  54. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/realtime/client.py +0 -0
  55. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/realtime/gateway_manager.py +0 -0
  56. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/realtime/push_to_talk.py +0 -0
  57. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/realtime/session.py +0 -0
  58. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/realtime/session_controller.py +0 -0
  59. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/realtime/talk_ui.py +0 -0
  60. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/realtime/wire_encode.py +0 -0
  61. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/scaffold_defaults/AGENT.md +0 -0
  62. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/scaffold_defaults/Dockerfile +0 -0
  63. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/scaffold_defaults/__init__.py +0 -0
  64. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/scaffold_defaults/browser/SKILL.md +0 -0
  65. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/scaffold_defaults/dockerignore +0 -0
  66. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/scaffold_defaults/env.example +0 -0
  67. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/scaffold_defaults/goal/SKILL.md +0 -0
  68. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/scaffold_defaults/image-generator/SKILL.md +0 -0
  69. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/scaffold_defaults/image-generator/generate_image.py +0 -0
  70. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/scaffold_defaults/loop/SKILL.md +0 -0
  71. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/scaffold_defaults/mcp.json +0 -0
  72. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/scaffold_defaults/opensandbox.docker.toml +0 -0
  73. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/scaffold_defaults/otel-collector.example.yaml +0 -0
  74. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/scaffold_defaults/permissions.yaml +0 -0
  75. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/session_controller.py +0 -0
  76. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/src/monkeybot_cli/terminal_markdown.py +0 -0
  77. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_chat_continue.py +0 -0
  78. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_chat_e2e.py +0 -0
  79. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_chat_errors.py +0 -0
  80. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_chat_file_index.py +0 -0
  81. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_chat_grounding.py +0 -0
  82. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_chat_local_shell.py +0 -0
  83. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_chat_renderer_parity.py +0 -0
  84. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_chat_session.py +0 -0
  85. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_chat_status_bar.py +0 -0
  86. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_chat_theme.py +0 -0
  87. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_chat_tool_display.py +0 -0
  88. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_config_resolve.py +0 -0
  89. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_doctor.py +0 -0
  90. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_extras_catalog.py +0 -0
  91. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_gateway_health.py +0 -0
  92. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_opensandbox_lifecycle.py +0 -0
  93. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_realtime_session_controller.py +0 -0
  94. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_run_cmd.py +0 -0
  95. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_terminal_markdown.py +0 -0
  96. {monkeybot_cli-0.10.0 → monkeybot_cli-0.11.1}/tests/test_trace_cmd.py +0 -0
@@ -0,0 +1,90 @@
1
+ # Internal (not shipped in OSS)
2
+ internal/
3
+
4
+ # Environment
5
+ .env
6
+ .env.*
7
+ !.env.example
8
+ # The grants fixture's .env is fixture data, not a local secret.
9
+ !regression/fixtures/workspaces/grants_ws/granted/.env
10
+
11
+ # Local scaffolded harness config (defaults ship in monkeybot-cli scaffold_defaults/)
12
+ /monkeybot_config/
13
+
14
+ # Live eval run artifacts. Baselines under regression/t4/baselines/ stay versioned.
15
+ /regression/t4/runs/
16
+ # Live-eval agent project local state. Live secrets come from the repo-root .env; a stray key
17
+ # file in the agent project stays out of git. The two workspace .gitkeep files need git add -f.
18
+ /regression/t4/agent/.env
19
+ /regression/t4/agent/data/
20
+ /regression/t4/agent/workspace/
21
+
22
+ # Python
23
+ __pycache__/
24
+ *.py[cod]
25
+ *.pyo
26
+ *.pyd
27
+ .Python
28
+ *.egg-info/
29
+ dist/
30
+ build/
31
+ .eggs/
32
+ *.egg
33
+
34
+ # uv
35
+ .venv/
36
+ .uv/
37
+
38
+ # Data (SQLite DB + memory files — never commit)
39
+ data/
40
+ # Shared attachment fixtures live here (§5.4); they are not a local data dir.
41
+ !testkit/data/
42
+ !testkit/data/**
43
+ /memory/
44
+ .monkeybot/
45
+ # Store layout for the attachment loop fixture. The directory name is ignored above.
46
+ !regression/fixtures/workspaces/att_ws/.monkeybot/
47
+ !regression/fixtures/workspaces/att_ws/.monkeybot/**
48
+ *.sqlite3
49
+ workspace/.monkeybot/
50
+ *.sqlite
51
+ *.sqlite.writer-pid
52
+
53
+ # Testing & coverage
54
+ /test-runs/
55
+ /.cursor/hooks/.state/
56
+ .pytest_cache/
57
+ .coverage
58
+ htmlcov/
59
+ .mypy_cache/
60
+ .ruff_cache/
61
+ .deepeval/
62
+
63
+ # macOS
64
+ .DS_Store
65
+
66
+ # IDEs
67
+ .idea/
68
+ .vscode/
69
+ *.swp
70
+ *.swo
71
+
72
+ # Docker
73
+ docker/data/
74
+
75
+ # Planning Documents
76
+ .prt/
77
+ .monkeymode/
78
+
79
+ # Reference clones (read-only)
80
+ code/
81
+
82
+ # Local venv for evals dependency experiments
83
+ .evals-venv/
84
+
85
+ # monkeythink
86
+ .monkeythink/
87
+ # Eval fixtures ship their own build/ decoy
88
+ !/regression/fixtures/workspaces/calc_repo/build/
89
+ !/regression/fixtures/workspaces/calc_repo_green/build/
90
+ !/regression/fixtures/workspaces/shop_repo/build/
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: monkeybot-cli
3
- Version: 0.10.0
3
+ Version: 0.11.1
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.9.0
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.10.0"
7
+ version = "0.11.1"
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.9.0,<4",
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",
@@ -21,6 +21,12 @@ dependencies = [
21
21
  [project.scripts]
22
22
  monkeybot = "monkeybot_cli.main:main"
23
23
 
24
+ [tool.uv]
25
+ # browser-harness 0.1.13 pins pillow==12.3.0, which is still inside the Pillow 13 advisory range.
26
+ # uv lock and uv sync honor this. uv tool install and uv pip install do not;
27
+ # those commands pass src/monkeybot_cli/overrides.txt via --overrides.
28
+ override-dependencies = ["pillow>=13"]
29
+
24
30
  [tool.uv.sources]
25
31
  monkeybot = { path = "..", editable = true }
26
32
  monkeybot-browser-mcp = { path = "../integrations/browser-mcp", editable = true }
@@ -176,7 +176,7 @@ Only if the user is changing monkeybot itself (not creating an agent):
176
176
  git clone https://github.com/human-plus-machine/monkeybot.git
177
177
  cd monkeybot && uv sync
178
178
  cd cli && uv sync
179
- uv tool install --editable .
179
+ uv tool install --editable . --overrides src/monkeybot_cli/overrides.txt
180
180
  ```
181
181
 
182
182
  ## Rules
@@ -84,7 +84,9 @@ MemPalace capture, wake-up, and search teaching are on by default. Turn them off
84
84
  |---|---|---|
85
85
  | `enabled` | `true` | Set `false` to skip capture, wake-up, and prompt teaching |
86
86
  | `backend` | `chroma` | Alternate MemPalace vector backend |
87
- | `embedding_model` | `embeddinggemma-300m` | Match the embedder the palace was built with |
87
+ | `embedding_model` | `minilm` | MemPalace's default embedder. `embeddinggemma-300m` is a legacy alias; startup rewrites it to `minilm` |
88
+
89
+ Chats in one project share saved lines by default, including the wake-up briefing. Set `MONKEYBOT_MEMORY_SHARE_THREADS=0` to keep each chat's lines in that chat. Unset, `1`, `true`, `yes`, and `on` share.
88
90
 
89
91
  ## `subagents`
90
92
 
@@ -28,6 +28,10 @@ from monkeybot_cli.chat_tool_display import format_tool_expand_body
28
28
  _SPIN = "⠋⠙⠹⠸⠴⠦⠧⠇⠏"
29
29
  _HISTORY_LIMIT = 500
30
30
  _COMPOSER_PLACEHOLDER = "Message the agent — / commands · @ files · ! shell"
31
+ # Enter's raw bytes. modifyOtherKeys and a few terminals report Shift/Alt+Enter
32
+ # as these characters instead of the name "enter".
33
+ _ENTER_KEY_NAMES = frozenset({"enter", "\r", "\n"})
34
+ _NEWLINE_MODIFIERS = frozenset({"shift", "alt"})
31
35
 
32
36
 
33
37
  def _plain_title(text: str) -> Content:
@@ -747,6 +751,50 @@ class ShortcutsScreen(ModalScreen[None]):
747
751
  self.dismiss(None)
748
752
 
749
753
 
754
+ def is_newline_key(key: str) -> bool:
755
+ """True when this key should insert a newline instead of sending.
756
+
757
+ Kitty-protocol terminals report ``shift+enter``. modifyOtherKeys and some
758
+ macOS terminals report the same chord as ``shift+`` plus a raw CR or LF.
759
+ ``ctrl+j`` is the chord every terminal can deliver.
760
+ """
761
+ if key in {"shift+enter", "alt+enter", "ctrl+j", "newline"}:
762
+ return True
763
+ parts = key.split("+")
764
+ if len(parts) < 2 or parts[-1] not in _ENTER_KEY_NAMES:
765
+ return False
766
+ return bool(_NEWLINE_MODIFIERS.intersection(parts[:-1]))
767
+
768
+
769
+ def install_terminal_newline_keys() -> None:
770
+ """Teach Textual's parser the Enter chords it currently mislabels.
771
+
772
+ Alt+Enter arrives as ESC then CR, and Textual drops the Alt because Enter
773
+ is not a single-character key. modifyOtherKeys Shift+Enter arrives as
774
+ ``shift+`` plus a raw carriage return, which matches no binding.
775
+ """
776
+ from textual._xterm_parser import XTermParser
777
+
778
+ current = XTermParser._sequence_to_key_events
779
+ if getattr(current, "_monkeybot_newline", False):
780
+ return
781
+
782
+ def _sequence_to_key_events(self: XTermParser, sequence: str, alt: bool = False): # type: ignore[no-untyped-def]
783
+ for event in current(self, sequence, alt=alt):
784
+ key = event.key
785
+ if alt and key == "enter":
786
+ yield Key("alt+enter", None)
787
+ elif key in {"shift+\r", "shift+\n"}:
788
+ yield Key("shift+enter", None)
789
+ elif key in {"alt+\r", "alt+\n"}:
790
+ yield Key("alt+enter", None)
791
+ else:
792
+ yield event
793
+
794
+ _sequence_to_key_events._monkeybot_newline = True # type: ignore[attr-defined]
795
+ XTermParser._sequence_to_key_events = _sequence_to_key_events # type: ignore[method-assign]
796
+
797
+
750
798
  class Composer(TextArea):
751
799
  """Growing multiline input: Enter sends, Shift+Enter/Ctrl+J newline, history, slash."""
752
800
 
@@ -1003,6 +1051,11 @@ class Composer(TextArea):
1003
1051
  if self._search_mode:
1004
1052
  self._handle_search_key(event)
1005
1053
  return
1054
+ if is_newline_key(event.key):
1055
+ self.action_insert_newline()
1056
+ event.prevent_default()
1057
+ event.stop()
1058
+ return
1006
1059
  if event.character == "?" and not self.text.strip():
1007
1060
  with contextlib.suppress(Exception):
1008
1061
  app = self.app
@@ -1054,7 +1107,8 @@ class Composer(TextArea):
1054
1107
  event.prevent_default()
1055
1108
  event.stop()
1056
1109
  return
1057
- super()._on_key(event)
1110
+ # TextArea._on_key runs next via the message MRO and inserts printable
1111
+ # characters. Calling it here as well only creates an unawaited coroutine.
1058
1112
  self.call_after_refresh(self._sync_height)
1059
1113
 
1060
1114
  def _handle_search_key(self, event: Key) -> None:
@@ -1115,3 +1169,6 @@ class TranscriptPane(VerticalScroll):
1115
1169
  on_scroll = getattr(app, "_on_transcript_scroll", None)
1116
1170
  if callable(on_scroll):
1117
1171
  on_scroll()
1172
+
1173
+
1174
+ install_terminal_newline_keys()
@@ -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
- def _prompt_single(choices: tuple[ExtraChoice, ...], *, title: str, default_key: str) -> str:
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 provider [{default_idx}]: ").strip()
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 provider {raw!r}; using {default_key}", file=sys.stderr)
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,
@@ -167,6 +193,7 @@ def run_new_command(args: argparse.Namespace) -> int:
167
193
  provider=provider,
168
194
  model=model,
169
195
  extras=extras or None,
196
+ template=template or DEFAULT_TEMPLATE,
170
197
  )
171
198
  print(f"monkeybot scaffold under {dest}:")
172
199
  print("\n".join(report))
@@ -177,6 +204,12 @@ def run_new_command(args: argparse.Namespace) -> int:
177
204
  print(" cp .env.example .env # then add provider keys")
178
205
  print(" monkeybot doctor")
179
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.")
180
213
  return 0
181
214
 
182
215
 
@@ -200,6 +233,13 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
200
233
  "observability, openai, bedrock, claude"
201
234
  ),
202
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
+ )
203
243
  p.add_argument("--force", action="store_true", help="Overwrite existing scaffold files")
204
244
  p.add_argument("--yes", "-y", action="store_true", help="Skip interactive prompts")
205
245
  p.set_defaults(func=run_new_command)
@@ -5,7 +5,18 @@ from __future__ import annotations
5
5
  import argparse
6
6
  import sys
7
7
 
8
- from monkeybot_cli.commands import chat, doctor, loop, new, refresh, run_cmd, talk, trace, validate
8
+ from monkeybot_cli.commands import (
9
+ chat,
10
+ doctor,
11
+ loop,
12
+ mcp_login,
13
+ new,
14
+ refresh,
15
+ run_cmd,
16
+ talk,
17
+ trace,
18
+ validate,
19
+ )
9
20
  from monkeybot_cli.runtime_python import RuntimeUpgradeError, report_runtime_upgrade_error
10
21
 
11
22
 
@@ -25,6 +36,7 @@ def build_parser() -> argparse.ArgumentParser:
25
36
  talk.register(sub)
26
37
  loop.register(sub)
27
38
  trace.register(sub)
39
+ mcp_login.register(sub)
28
40
  return parser
29
41
 
30
42
 
@@ -0,0 +1,5 @@
1
+ # browser-harness 0.1.13 requires pillow==12.3.0. That release is inside
2
+ # advisories fixed in Pillow 13. [tool.uv] override-dependencies applies this
3
+ # floor to uv lock and uv sync only. uv tool install and uv pip install need
4
+ # this file via --overrides.
5
+ pillow>=13