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.
- monkeybot_cli/__init__.py +3 -0
- monkeybot_cli/chat_renderer.py +87 -0
- monkeybot_cli/chat_session.py +911 -0
- monkeybot_cli/chat_status_bar.py +205 -0
- monkeybot_cli/chat_theme.py +91 -0
- monkeybot_cli/chat_tool_display.py +334 -0
- monkeybot_cli/chat_tui.py +1491 -0
- monkeybot_cli/chat_tui_widgets.py +996 -0
- monkeybot_cli/commands/__init__.py +1 -0
- monkeybot_cli/commands/chat.py +817 -0
- monkeybot_cli/commands/doctor.py +293 -0
- monkeybot_cli/commands/loop.py +207 -0
- monkeybot_cli/commands/new.py +207 -0
- monkeybot_cli/commands/run_cmd.py +41 -0
- monkeybot_cli/commands/talk.py +102 -0
- monkeybot_cli/commands/validate.py +385 -0
- monkeybot_cli/compat.py +7 -0
- monkeybot_cli/config_resolve.py +55 -0
- monkeybot_cli/exit_commands.py +13 -0
- monkeybot_cli/extras_catalog.py +95 -0
- monkeybot_cli/gateway_health.py +34 -0
- monkeybot_cli/main.py +38 -0
- monkeybot_cli/opensandbox_lifecycle.py +314 -0
- monkeybot_cli/output.py +110 -0
- monkeybot_cli/providers.py +112 -0
- monkeybot_cli/realtime/__init__.py +13 -0
- monkeybot_cli/realtime/audio_io.py +147 -0
- monkeybot_cli/realtime/client.py +17 -0
- monkeybot_cli/realtime/gateway_manager.py +142 -0
- monkeybot_cli/realtime/push_to_talk.py +128 -0
- monkeybot_cli/realtime/session.py +256 -0
- monkeybot_cli/realtime/session_controller.py +501 -0
- monkeybot_cli/realtime/talk_ui.py +243 -0
- monkeybot_cli/realtime/wire_encode.py +39 -0
- monkeybot_cli/runtime_python.py +91 -0
- monkeybot_cli/scaffold.py +287 -0
- monkeybot_cli/scaffold_defaults/AGENT.md +56 -0
- monkeybot_cli/scaffold_defaults/__init__.py +1 -0
- monkeybot_cli/scaffold_defaults/command_allowlist.yaml +57 -0
- monkeybot_cli/scaffold_defaults/env.example +35 -0
- monkeybot_cli/scaffold_defaults/mcp.json +49 -0
- monkeybot_cli/scaffold_defaults/monkeybot.example.yaml +191 -0
- monkeybot_cli/scaffold_defaults/otel-collector.example.yaml +57 -0
- monkeybot_cli/scaffold_defaults/permissions.yaml +32 -0
- monkeybot_cli/scaffold_defaults/setup-workspace.sh +24 -0
- monkeybot_cli/session_controller.py +7 -0
- monkeybot_cli/terminal_markdown.py +48 -0
- monkeybot_cli-0.2.1.dist-info/METADATA +10 -0
- monkeybot_cli-0.2.1.dist-info/RECORD +51 -0
- monkeybot_cli-0.2.1.dist-info/WHEEL +4 -0
- 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)
|