monkeybot-cli 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. monkeybot_cli/__init__.py +3 -0
  2. monkeybot_cli/chat_renderer.py +87 -0
  3. monkeybot_cli/chat_session.py +911 -0
  4. monkeybot_cli/chat_status_bar.py +205 -0
  5. monkeybot_cli/chat_theme.py +91 -0
  6. monkeybot_cli/chat_tool_display.py +334 -0
  7. monkeybot_cli/chat_tui.py +1491 -0
  8. monkeybot_cli/chat_tui_widgets.py +996 -0
  9. monkeybot_cli/commands/__init__.py +1 -0
  10. monkeybot_cli/commands/chat.py +817 -0
  11. monkeybot_cli/commands/doctor.py +293 -0
  12. monkeybot_cli/commands/loop.py +207 -0
  13. monkeybot_cli/commands/new.py +207 -0
  14. monkeybot_cli/commands/run_cmd.py +41 -0
  15. monkeybot_cli/commands/talk.py +102 -0
  16. monkeybot_cli/commands/validate.py +385 -0
  17. monkeybot_cli/compat.py +7 -0
  18. monkeybot_cli/config_resolve.py +55 -0
  19. monkeybot_cli/exit_commands.py +13 -0
  20. monkeybot_cli/extras_catalog.py +95 -0
  21. monkeybot_cli/gateway_health.py +34 -0
  22. monkeybot_cli/main.py +38 -0
  23. monkeybot_cli/opensandbox_lifecycle.py +314 -0
  24. monkeybot_cli/output.py +110 -0
  25. monkeybot_cli/providers.py +112 -0
  26. monkeybot_cli/realtime/__init__.py +13 -0
  27. monkeybot_cli/realtime/audio_io.py +147 -0
  28. monkeybot_cli/realtime/client.py +17 -0
  29. monkeybot_cli/realtime/gateway_manager.py +142 -0
  30. monkeybot_cli/realtime/push_to_talk.py +128 -0
  31. monkeybot_cli/realtime/session.py +256 -0
  32. monkeybot_cli/realtime/session_controller.py +501 -0
  33. monkeybot_cli/realtime/talk_ui.py +243 -0
  34. monkeybot_cli/realtime/wire_encode.py +39 -0
  35. monkeybot_cli/runtime_python.py +91 -0
  36. monkeybot_cli/scaffold.py +287 -0
  37. monkeybot_cli/scaffold_defaults/AGENT.md +56 -0
  38. monkeybot_cli/scaffold_defaults/__init__.py +1 -0
  39. monkeybot_cli/scaffold_defaults/command_allowlist.yaml +57 -0
  40. monkeybot_cli/scaffold_defaults/env.example +35 -0
  41. monkeybot_cli/scaffold_defaults/mcp.json +49 -0
  42. monkeybot_cli/scaffold_defaults/monkeybot.example.yaml +191 -0
  43. monkeybot_cli/scaffold_defaults/otel-collector.example.yaml +57 -0
  44. monkeybot_cli/scaffold_defaults/permissions.yaml +32 -0
  45. monkeybot_cli/scaffold_defaults/setup-workspace.sh +24 -0
  46. monkeybot_cli/session_controller.py +7 -0
  47. monkeybot_cli/terminal_markdown.py +48 -0
  48. monkeybot_cli-0.2.1.dist-info/METADATA +10 -0
  49. monkeybot_cli-0.2.1.dist-info/RECORD +51 -0
  50. monkeybot_cli-0.2.1.dist-info/WHEEL +4 -0
  51. monkeybot_cli-0.2.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,207 @@
1
+ """monkeybot new — scaffold a bot workspace."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from monkeybot_cli.extras_catalog import (
10
+ FEATURE_CHOICES,
11
+ PROVIDER_CHOICES,
12
+ ExtraChoice,
13
+ additional_provider_extra_choices,
14
+ normalize_extra_token,
15
+ )
16
+ from monkeybot_cli.scaffold import run_new
17
+
18
+ _DEFAULT_PROVIDER = "gemini"
19
+ _DEFAULT_MODEL = "gemini-3-flash"
20
+
21
+
22
+ def _prompt_line(prompt: str) -> str:
23
+ try:
24
+ return input(prompt)
25
+ except EOFError:
26
+ return ""
27
+
28
+
29
+ def _prompt_single(choices: tuple[ExtraChoice, ...], *, title: str, default_key: str) -> str:
30
+ """Numbered single-select; Enter keeps default_key."""
31
+ print(title)
32
+ default_idx = 1
33
+ for i, choice in enumerate(choices, start=1):
34
+ marker = " (default)" if choice.key == default_key else ""
35
+ if choice.key == default_key:
36
+ default_idx = i
37
+ print(f" {i}) {choice.key} — {choice.label}{marker}")
38
+ raw = _prompt_line(f"Choose provider [{default_idx}]: ").strip()
39
+ if not raw:
40
+ return default_key
41
+ if raw.isdigit():
42
+ idx = int(raw)
43
+ if 1 <= idx <= len(choices):
44
+ return choices[idx - 1].key
45
+ print(f" invalid number; using {default_key}", file=sys.stderr)
46
+ return default_key
47
+ # Allow typing the key / alias directly
48
+ for choice in choices:
49
+ if raw.lower() == choice.key.lower():
50
+ return choice.key
51
+ print(f" unknown provider {raw!r}; using {default_key}", file=sys.stderr)
52
+ return default_key
53
+
54
+
55
+ def _prompt_multi(choices: tuple[ExtraChoice, ...], *, title: str) -> list[str]:
56
+ """Numbered multi-select (comma-separated). Enter = none."""
57
+ if not choices:
58
+ return []
59
+ print(title)
60
+ print(" (comma-separated numbers or names; Enter for none)")
61
+ for i, choice in enumerate(choices, start=1):
62
+ print(f" {i}) {choice.key} — {choice.label}")
63
+ raw = _prompt_line("Select: ").strip()
64
+ if not raw:
65
+ return []
66
+ selected: list[str] = []
67
+ seen: set[str] = set()
68
+ for part in raw.split(","):
69
+ token = part.strip()
70
+ if not token:
71
+ continue
72
+ key: str | None = None
73
+ if token.isdigit():
74
+ idx = int(token)
75
+ if 1 <= idx <= len(choices):
76
+ key = choices[idx - 1].key
77
+ else:
78
+ print(f" skipping invalid number {token}", file=sys.stderr)
79
+ continue
80
+ else:
81
+ for choice in choices:
82
+ if token.lower() == choice.key.lower():
83
+ key = choice.key
84
+ break
85
+ if key is None:
86
+ normalized = normalize_extra_token(token)
87
+ if normalized is not None:
88
+ key = normalized
89
+ else:
90
+ print(f" skipping unknown option {token!r}", file=sys.stderr)
91
+ continue
92
+ if key not in seen:
93
+ seen.add(key)
94
+ selected.append(key)
95
+ return selected
96
+
97
+
98
+ def _resolve_extras_from_args(with_args: list[str] | None) -> tuple[list[str], list[str]]:
99
+ """Return (extras, unknown_tokens)."""
100
+ extras: list[str] = []
101
+ unknown: list[str] = []
102
+ seen: set[str] = set()
103
+ for raw in with_args or ():
104
+ for part in raw.split(","):
105
+ token = part.strip()
106
+ if not token:
107
+ continue
108
+ normalized = normalize_extra_token(token)
109
+ if normalized is None:
110
+ # fake maps to None — treat as skip only if token is fake
111
+ if token.lower() in {"fake"}:
112
+ continue
113
+ unknown.append(token)
114
+ continue
115
+ if normalized not in seen:
116
+ seen.add(normalized)
117
+ extras.append(normalized)
118
+ return extras, unknown
119
+
120
+
121
+ def run_new_command(args: argparse.Namespace) -> int:
122
+ dest = Path(args.dest).expanduser().resolve()
123
+ if not dest.exists():
124
+ dest.mkdir(parents=True, exist_ok=True)
125
+ if not dest.is_dir():
126
+ print(f"error: --dest is not a directory: {dest}", file=sys.stderr)
127
+ return 2
128
+
129
+ provider = args.provider
130
+ model = args.model
131
+ extras, unknown = _resolve_extras_from_args(getattr(args, "with_extras", None))
132
+ if unknown:
133
+ print(
134
+ f"error: unknown --with value(s): {', '.join(unknown)}",
135
+ file=sys.stderr,
136
+ )
137
+ return 2
138
+
139
+ if not args.yes:
140
+ if provider is None:
141
+ provider = _prompt_single(
142
+ PROVIDER_CHOICES,
143
+ title="Model provider:",
144
+ default_key=_DEFAULT_PROVIDER,
145
+ )
146
+ if model is None:
147
+ model = (
148
+ _prompt_line(f"Model name [{_DEFAULT_MODEL}]: ").strip() or _DEFAULT_MODEL
149
+ )
150
+ # Only prompt for optional deps when --with was not already given.
151
+ if not extras:
152
+ more_providers = additional_provider_extra_choices(provider)
153
+ extras.extend(
154
+ _prompt_multi(
155
+ more_providers,
156
+ title="Additional providers to install (optional):",
157
+ )
158
+ )
159
+ extras.extend(
160
+ _prompt_multi(
161
+ FEATURE_CHOICES,
162
+ title="Optional features to install:",
163
+ )
164
+ )
165
+
166
+ report = run_new(
167
+ dest=dest,
168
+ force=args.force,
169
+ provider=provider,
170
+ model=model,
171
+ extras=extras or None,
172
+ )
173
+ print(f"monkeybot scaffold under {dest}:")
174
+ print("\n".join(report))
175
+ print()
176
+ print("Next:")
177
+ print(f" cd {dest}")
178
+ print(" uv sync")
179
+ print(" cp .env.example .env # then add provider keys")
180
+ print(" monkeybot doctor")
181
+ print(" monkeybot chat")
182
+ return 0
183
+
184
+
185
+ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
186
+ p = subparsers.add_parser(
187
+ "new",
188
+ help="Scaffold monkeybot_config/, workspace dirs, and agent pyproject.toml",
189
+ )
190
+ p.add_argument("--dest", type=Path, default=Path.cwd(), help="Root directory for the bot")
191
+ p.add_argument("--provider", help="model.provider value for monkeybot.yaml")
192
+ p.add_argument("--model", help="model.name value for monkeybot.yaml")
193
+ p.add_argument(
194
+ "--with",
195
+ dest="with_extras",
196
+ action="append",
197
+ default=[],
198
+ metavar="EXTRA",
199
+ help=(
200
+ "Optional dependency to include in agent pyproject.toml "
201
+ "(repeatable or comma-separated). Examples: postgres, sandbox, "
202
+ "observability, openai, bedrock, claude"
203
+ ),
204
+ )
205
+ p.add_argument("--force", action="store_true", help="Overwrite existing scaffold files")
206
+ p.add_argument("--yes", "-y", action="store_true", help="Skip interactive prompts")
207
+ p.set_defaults(func=run_new_command)
@@ -0,0 +1,41 @@
1
+ """monkeybot run — launch the SSE gateway as a subprocess."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import subprocess
8
+ from pathlib import Path
9
+
10
+ from monkeybot_cli.config_resolve import load_agent_dotenv, resolve_agent_root, resolve_config
11
+ from monkeybot_cli.runtime_python import gateway_argv, resolve_runtime_python
12
+
13
+
14
+ def run_run(args: argparse.Namespace) -> int:
15
+ cwd = Path(args.cwd).expanduser().resolve() if args.cwd else None
16
+ config_path = resolve_config(args.config, cwd=cwd)
17
+ load_agent_dotenv(cwd=cwd, config_path=config_path)
18
+ env = os.environ.copy()
19
+ if config_path is not None:
20
+ env["MONKEYBOT_CONFIG"] = str(config_path)
21
+ if args.port:
22
+ env["PORT"] = str(args.port)
23
+ # Derive the agent root from --cwd when given, else from --config so an
24
+ # off-tree config selects the right project venv / `uv run` cwd — matching
25
+ # `monkeybot doctor`. Falls back to cwd when neither is provided.
26
+ agent_root = resolve_agent_root(cwd=cwd, config_path=config_path)
27
+ runtime = resolve_runtime_python(agent_root)
28
+ cmd = gateway_argv(runtime)
29
+ try:
30
+ proc = subprocess.run(cmd, env=env, cwd=agent_root)
31
+ return proc.returncode
32
+ except KeyboardInterrupt:
33
+ return 130
34
+
35
+
36
+ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
37
+ p = subparsers.add_parser("run", help="Start the monkeybot SSE gateway")
38
+ p.add_argument("--config", help="Path to monkeybot.yaml (sets MONKEYBOT_CONFIG)")
39
+ p.add_argument("--port", type=int, help="Listen port (sets PORT)")
40
+ p.add_argument("--cwd", help="Working directory for the gateway process")
41
+ p.set_defaults(func=run_run)
@@ -0,0 +1,102 @@
1
+ """monkeybot talk — realtime WebSocket client (text and/or audio)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from monkeybot_cli.config_resolve import load_config_doc, resolve_config
11
+ from monkeybot_cli.realtime.session import run_talk_session
12
+ from monkeybot_cli.runtime_python import DEFAULT_PORT
13
+
14
+
15
+ def _gateway_url_from_args(args: argparse.Namespace) -> str:
16
+ if getattr(args, "gateway_url", None):
17
+ return str(args.gateway_url).rstrip("/")
18
+ env = os.getenv("MONKEYBOT_GATEWAY_URL")
19
+ if env:
20
+ return env.rstrip("/")
21
+ cwd = Path(args.cwd).expanduser().resolve() if getattr(args, "cwd", None) else None
22
+ config_path = resolve_config(getattr(args, "config", None), cwd=cwd)
23
+ port = DEFAULT_PORT
24
+ if config_path is not None:
25
+ _, doc = load_config_doc(str(config_path))
26
+ runtime = doc.get("runtime") if isinstance(doc.get("runtime"), dict) else {}
27
+ try:
28
+ port = int(runtime.get("port", DEFAULT_PORT))
29
+ except (TypeError, ValueError):
30
+ port = DEFAULT_PORT
31
+ return f"ws://127.0.0.1:{port}"
32
+
33
+
34
+ def run_talk(args: argparse.Namespace) -> int:
35
+ """Connect to the realtime gateway (audio by default, or --text)."""
36
+ return run_talk_session(
37
+ gateway_url=_gateway_url_from_args(args),
38
+ session_id=args.session_id,
39
+ text=bool(args.text),
40
+ ptt_key=args.ptt_key,
41
+ start_gateway=not args.no_start_gateway,
42
+ input_format=args.input_format,
43
+ chunk_ms=args.chunk_ms,
44
+ verbose=bool(args.verbose),
45
+ )
46
+
47
+
48
+ def register(subparsers: Any) -> None:
49
+ p = subparsers.add_parser(
50
+ "talk",
51
+ help="Talk with a realtime agent (WebSocket; audio by default)",
52
+ )
53
+ p.add_argument("--cwd", help="Agent root (defaults to the current directory)")
54
+ p.add_argument(
55
+ "--config", help="Path to monkeybot.yaml (defaults to ./monkeybot_config/monkeybot.yaml)"
56
+ )
57
+ p.add_argument(
58
+ "--gateway-url",
59
+ default=None,
60
+ help=(
61
+ "Realtime gateway base URL "
62
+ f"(env: MONKEYBOT_GATEWAY_URL; default ws://127.0.0.1:{{runtime.port|{DEFAULT_PORT}}})"
63
+ ),
64
+ )
65
+ p.add_argument(
66
+ "--session-id",
67
+ default=os.getenv("MONKEYBOT_SESSION_ID"),
68
+ help="Session ID (generated if omitted; env: MONKEYBOT_SESSION_ID)",
69
+ )
70
+ p.add_argument(
71
+ "--text",
72
+ action="store_true",
73
+ help="Text-only input (no microphone); uses the chat TUI when on a TTY",
74
+ )
75
+ p.add_argument(
76
+ "--ptt-key",
77
+ default="cmd",
78
+ choices=("cmd", "alt", "ctrl", "space"),
79
+ help="Push-to-talk key to hold while speaking (default: cmd)",
80
+ )
81
+ p.add_argument(
82
+ "--no-start-gateway",
83
+ action="store_true",
84
+ help="Do not auto-start a local realtime gateway",
85
+ )
86
+ p.add_argument(
87
+ "--input-format",
88
+ default="pcm_s16le_24khz_mono",
89
+ help="Audio format, e.g. pcm_s16le_24khz_mono",
90
+ )
91
+ p.add_argument(
92
+ "--chunk-ms",
93
+ type=int,
94
+ default=200,
95
+ help="Audio chunk size in milliseconds",
96
+ )
97
+ p.add_argument(
98
+ "--verbose",
99
+ action="store_true",
100
+ help="Enable debug logging for audio chunks and gateway events",
101
+ )
102
+ p.set_defaults(func=run_talk)