batrachian-toad 0.5.22__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.
- batrachian_toad-0.5.22.dist-info/METADATA +197 -0
- batrachian_toad-0.5.22.dist-info/RECORD +120 -0
- batrachian_toad-0.5.22.dist-info/WHEEL +4 -0
- batrachian_toad-0.5.22.dist-info/entry_points.txt +2 -0
- batrachian_toad-0.5.22.dist-info/licenses/LICENSE +661 -0
- toad/__init__.py +46 -0
- toad/__main__.py +4 -0
- toad/_loop.py +86 -0
- toad/about.py +90 -0
- toad/acp/agent.py +671 -0
- toad/acp/api.py +47 -0
- toad/acp/encode_tool_call_id.py +12 -0
- toad/acp/messages.py +138 -0
- toad/acp/prompt.py +54 -0
- toad/acp/protocol.py +426 -0
- toad/agent.py +62 -0
- toad/agent_schema.py +70 -0
- toad/agents.py +45 -0
- toad/ansi/__init__.py +1 -0
- toad/ansi/_ansi.py +1612 -0
- toad/ansi/_ansi_colors.py +264 -0
- toad/ansi/_control_codes.py +37 -0
- toad/ansi/_keys.py +251 -0
- toad/ansi/_sgr_styles.py +64 -0
- toad/ansi/_stream_parser.py +418 -0
- toad/answer.py +22 -0
- toad/app.py +557 -0
- toad/atomic.py +37 -0
- toad/cli.py +257 -0
- toad/code_analyze.py +28 -0
- toad/complete.py +34 -0
- toad/constants.py +58 -0
- toad/conversation_markdown.py +19 -0
- toad/danger.py +371 -0
- toad/data/agents/ampcode.com.toml +51 -0
- toad/data/agents/augmentcode.com.toml +40 -0
- toad/data/agents/claude.com.toml +41 -0
- toad/data/agents/docker.com.toml +59 -0
- toad/data/agents/geminicli.com.toml +28 -0
- toad/data/agents/goose.ai.toml +51 -0
- toad/data/agents/inference.huggingface.co.toml +33 -0
- toad/data/agents/kimi.com.toml +35 -0
- toad/data/agents/openai.com.toml +53 -0
- toad/data/agents/opencode.ai.toml +61 -0
- toad/data/agents/openhands.dev.toml +44 -0
- toad/data/agents/stakpak.dev.toml +61 -0
- toad/data/agents/vibe.mistral.ai.toml +27 -0
- toad/data/agents/vtcode.dev.toml +62 -0
- toad/data/images/frog.png +0 -0
- toad/data/sounds/turn-over.wav +0 -0
- toad/db.py +5 -0
- toad/dec.py +332 -0
- toad/directory.py +234 -0
- toad/directory_watcher.py +96 -0
- toad/fuzzy.py +140 -0
- toad/gist.py +2 -0
- toad/history.py +138 -0
- toad/jsonrpc.py +576 -0
- toad/menus.py +14 -0
- toad/messages.py +74 -0
- toad/option_content.py +51 -0
- toad/os.py +0 -0
- toad/path_complete.py +145 -0
- toad/path_filter.py +124 -0
- toad/paths.py +71 -0
- toad/pill.py +23 -0
- toad/prompt/extract.py +19 -0
- toad/prompt/resource.py +68 -0
- toad/protocol.py +28 -0
- toad/screens/action_modal.py +94 -0
- toad/screens/agent_modal.py +172 -0
- toad/screens/command_edit_modal.py +58 -0
- toad/screens/main.py +192 -0
- toad/screens/permissions.py +390 -0
- toad/screens/permissions.tcss +72 -0
- toad/screens/settings.py +254 -0
- toad/screens/settings.tcss +101 -0
- toad/screens/store.py +476 -0
- toad/screens/store.tcss +261 -0
- toad/settings.py +354 -0
- toad/settings_schema.py +318 -0
- toad/shell.py +263 -0
- toad/shell_read.py +42 -0
- toad/slash_command.py +34 -0
- toad/toad.tcss +752 -0
- toad/version.py +80 -0
- toad/visuals/columns.py +273 -0
- toad/widgets/agent_response.py +79 -0
- toad/widgets/agent_thought.py +41 -0
- toad/widgets/command_pane.py +224 -0
- toad/widgets/condensed_path.py +93 -0
- toad/widgets/conversation.py +1626 -0
- toad/widgets/danger_warning.py +65 -0
- toad/widgets/diff_view.py +709 -0
- toad/widgets/flash.py +81 -0
- toad/widgets/future_text.py +126 -0
- toad/widgets/grid_select.py +223 -0
- toad/widgets/highlighted_textarea.py +180 -0
- toad/widgets/mandelbrot.py +294 -0
- toad/widgets/markdown_note.py +13 -0
- toad/widgets/menu.py +147 -0
- toad/widgets/non_selectable_label.py +5 -0
- toad/widgets/note.py +18 -0
- toad/widgets/path_search.py +381 -0
- toad/widgets/plan.py +180 -0
- toad/widgets/project_directory_tree.py +74 -0
- toad/widgets/prompt.py +741 -0
- toad/widgets/question.py +337 -0
- toad/widgets/shell_result.py +35 -0
- toad/widgets/shell_terminal.py +18 -0
- toad/widgets/side_bar.py +74 -0
- toad/widgets/slash_complete.py +211 -0
- toad/widgets/strike_text.py +66 -0
- toad/widgets/terminal.py +526 -0
- toad/widgets/terminal_tool.py +338 -0
- toad/widgets/throbber.py +90 -0
- toad/widgets/tool_call.py +303 -0
- toad/widgets/user_input.py +23 -0
- toad/widgets/version.py +5 -0
- toad/widgets/welcome.py +31 -0
toad/cli.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from toad.app import ToadApp
|
|
5
|
+
from toad.agent_schema import Agent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def check_directory(path: str) -> None:
|
|
9
|
+
"""Check a path is directory, or exit the app.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
path: Path to check.
|
|
13
|
+
"""
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
if not Path(path).resolve().is_dir():
|
|
17
|
+
print(f"Not a directory: {path}")
|
|
18
|
+
sys.exit(-1)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def get_agent_data(launch_agent) -> Agent | None:
|
|
22
|
+
launch_agent = launch_agent.lower()
|
|
23
|
+
|
|
24
|
+
from toad.agents import read_agents, AgentReadError
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
agents = await read_agents()
|
|
28
|
+
except AgentReadError:
|
|
29
|
+
agents = {}
|
|
30
|
+
|
|
31
|
+
for agent_data in agents.values():
|
|
32
|
+
if (
|
|
33
|
+
agent_data["short_name"].lower() == launch_agent
|
|
34
|
+
or agent_data["identity"].lower() == launch_agent
|
|
35
|
+
):
|
|
36
|
+
launch_agent = agent_data["identity"]
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
return agents.get(launch_agent)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DefaultCommandGroup(click.Group):
|
|
43
|
+
def parse_args(self, ctx, args):
|
|
44
|
+
if "--help" in args or "-h" in args:
|
|
45
|
+
return super().parse_args(ctx, args)
|
|
46
|
+
# Check if first arg is a known subcommand
|
|
47
|
+
if not args or args[0] not in self.commands:
|
|
48
|
+
# If not a subcommand, prepend the default command name
|
|
49
|
+
args.insert(0, "run")
|
|
50
|
+
return super().parse_args(ctx, args)
|
|
51
|
+
|
|
52
|
+
def format_usage(self, ctx, formatter):
|
|
53
|
+
formatter.write_usage(ctx.command_path, "[OPTIONS] PATH OR COMMAND [ARGS]...")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@click.group(cls=DefaultCommandGroup)
|
|
57
|
+
def main():
|
|
58
|
+
"""🐸 Toad — AI for your terminal."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# @click.group(invoke_without_command=True)
|
|
62
|
+
# @click.pass_context
|
|
63
|
+
@main.command("run")
|
|
64
|
+
@click.argument("project_dir", metavar="PATH", required=False, default=".")
|
|
65
|
+
@click.option("-a", "--agent", metavar="AGENT", default="")
|
|
66
|
+
@click.option(
|
|
67
|
+
"-p",
|
|
68
|
+
"--port",
|
|
69
|
+
metavar="PORT",
|
|
70
|
+
default=8000,
|
|
71
|
+
type=int,
|
|
72
|
+
help="Port to use in conjunction with --serve",
|
|
73
|
+
)
|
|
74
|
+
@click.option(
|
|
75
|
+
"-H",
|
|
76
|
+
"--host",
|
|
77
|
+
metavar="HOST",
|
|
78
|
+
default="localhost",
|
|
79
|
+
type=str,
|
|
80
|
+
help="Host to use in conjunction with --serve",
|
|
81
|
+
)
|
|
82
|
+
@click.option("-s", "--serve", is_flag=True, help="Serve Toad as a web application")
|
|
83
|
+
def run(port: int, host: str, serve: bool, project_dir: str = ".", agent: str = "1"):
|
|
84
|
+
"""Run an installed agent (same as `toad PATH`)."""
|
|
85
|
+
|
|
86
|
+
check_directory(project_dir)
|
|
87
|
+
|
|
88
|
+
if agent:
|
|
89
|
+
import asyncio
|
|
90
|
+
|
|
91
|
+
agent_data = asyncio.run(get_agent_data(agent))
|
|
92
|
+
else:
|
|
93
|
+
agent_data = None
|
|
94
|
+
|
|
95
|
+
app = ToadApp(
|
|
96
|
+
mode=None if agent_data else "store",
|
|
97
|
+
agent_data=agent_data,
|
|
98
|
+
project_dir=project_dir,
|
|
99
|
+
)
|
|
100
|
+
if serve:
|
|
101
|
+
import shlex
|
|
102
|
+
from textual_serve.server import Server
|
|
103
|
+
|
|
104
|
+
command_args = sys.argv
|
|
105
|
+
# Remove serve flag from args (could be either --serve or -s)
|
|
106
|
+
for flag in ["--serve", "-s"]:
|
|
107
|
+
try:
|
|
108
|
+
command_args.remove(flag)
|
|
109
|
+
break
|
|
110
|
+
except ValueError:
|
|
111
|
+
pass
|
|
112
|
+
serve_command = shlex.join(command_args)
|
|
113
|
+
server = Server(
|
|
114
|
+
serve_command,
|
|
115
|
+
host=host,
|
|
116
|
+
port=port,
|
|
117
|
+
title=serve_command,
|
|
118
|
+
)
|
|
119
|
+
server.serve()
|
|
120
|
+
else:
|
|
121
|
+
app.run()
|
|
122
|
+
app.run_on_exit()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@main.command("acp")
|
|
126
|
+
@click.argument("command", metavar="COMMAND")
|
|
127
|
+
@click.argument("project_dir", metavar="PATH", default=None)
|
|
128
|
+
@click.option(
|
|
129
|
+
"-t",
|
|
130
|
+
"--title",
|
|
131
|
+
metavar="TITLE",
|
|
132
|
+
help="Optional title to display in the status bar",
|
|
133
|
+
default=None,
|
|
134
|
+
)
|
|
135
|
+
@click.option("-d", "--project-dir", metavar="PATH", default=None)
|
|
136
|
+
@click.option(
|
|
137
|
+
"-p",
|
|
138
|
+
"--port",
|
|
139
|
+
metavar="PORT",
|
|
140
|
+
default=8000,
|
|
141
|
+
type=int,
|
|
142
|
+
help="Port to use in conjunction with --serve",
|
|
143
|
+
)
|
|
144
|
+
@click.option(
|
|
145
|
+
"-H",
|
|
146
|
+
"--host",
|
|
147
|
+
metavar="HOST",
|
|
148
|
+
default="localhost",
|
|
149
|
+
help="Host to use in conjunction with --serve",
|
|
150
|
+
)
|
|
151
|
+
@click.option("-s", "--serve", is_flag=True, help="Serve Toad as a web application")
|
|
152
|
+
def acp(
|
|
153
|
+
command: str,
|
|
154
|
+
host: str,
|
|
155
|
+
port: int,
|
|
156
|
+
title: str | None,
|
|
157
|
+
project_dir: str | None,
|
|
158
|
+
serve: bool = False,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Run an ACP agent from a command."""
|
|
161
|
+
|
|
162
|
+
from rich import print
|
|
163
|
+
|
|
164
|
+
from toad.agent_schema import Agent as AgentData
|
|
165
|
+
|
|
166
|
+
command_name = command.split(" ", 1)[0].lower()
|
|
167
|
+
identity = f"{command_name}.custom.batrachian.ai"
|
|
168
|
+
|
|
169
|
+
agent_data: AgentData = {
|
|
170
|
+
"identity": identity,
|
|
171
|
+
"name": title or command.partition(" ")[0],
|
|
172
|
+
"short_name": "agent",
|
|
173
|
+
"url": "https://github.com/batrachianai/toad",
|
|
174
|
+
"protocol": "acp",
|
|
175
|
+
"type": "coding",
|
|
176
|
+
"author_name": "Will McGugan",
|
|
177
|
+
"author_url": "https://willmcgugan.github.io/",
|
|
178
|
+
"publisher_name": "Will McGugan",
|
|
179
|
+
"publisher_url": "https://willmcgugan.github.io/",
|
|
180
|
+
"description": "Agent launched from CLI",
|
|
181
|
+
"tags": [],
|
|
182
|
+
"help": "",
|
|
183
|
+
"run_command": {"*": command},
|
|
184
|
+
"actions": {},
|
|
185
|
+
}
|
|
186
|
+
if serve:
|
|
187
|
+
import shlex
|
|
188
|
+
from textual_serve.server import Server
|
|
189
|
+
|
|
190
|
+
command_components = [sys.argv[0], "acp", command]
|
|
191
|
+
if project_dir:
|
|
192
|
+
command_components.append(f"--project-dir={project_dir}")
|
|
193
|
+
serve_command = shlex.join(command_components)
|
|
194
|
+
|
|
195
|
+
server = Server(
|
|
196
|
+
serve_command,
|
|
197
|
+
host=host,
|
|
198
|
+
port=port,
|
|
199
|
+
title=serve_command,
|
|
200
|
+
)
|
|
201
|
+
server.serve()
|
|
202
|
+
else:
|
|
203
|
+
app = ToadApp(agent_data=agent_data, project_dir=project_dir)
|
|
204
|
+
app.run()
|
|
205
|
+
app.run_on_exit()
|
|
206
|
+
|
|
207
|
+
print("")
|
|
208
|
+
print("[bold magenta]Thanks for trying out Toad!")
|
|
209
|
+
print("Please head to Discussions to share your experiences (good or bad).")
|
|
210
|
+
print("https://github.com/batrachianai/toad/discussions")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@main.command("settings")
|
|
214
|
+
def settings() -> None:
|
|
215
|
+
"""Settings information."""
|
|
216
|
+
app = ToadApp()
|
|
217
|
+
print(f"{app.settings_path}")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# @main.command("replay")
|
|
221
|
+
# @click.argument("path", metavar="PATH.jsonl")
|
|
222
|
+
# def replay(path: str) -> None:
|
|
223
|
+
# """Replay interaction from a jsonl file."""
|
|
224
|
+
# import time
|
|
225
|
+
|
|
226
|
+
# stdout = sys.stdout.buffer
|
|
227
|
+
# with open(path, "rb") as replay_file:
|
|
228
|
+
# for line in replay_file.readlines():
|
|
229
|
+
# time.sleep(0.1)
|
|
230
|
+
# stdout.write(line)
|
|
231
|
+
# stdout.flush()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@main.command("serve")
|
|
235
|
+
@click.option("-p", "--port", metavar="PORT", default=8000, type=int)
|
|
236
|
+
@click.option("-H", "--host", metavar="HOST", default="localhost")
|
|
237
|
+
def serve(port: int, host: str) -> None:
|
|
238
|
+
"""Serve Toad as a web application."""
|
|
239
|
+
from textual_serve.server import Server
|
|
240
|
+
|
|
241
|
+
server = Server(sys.argv[0], host=host, port=port, title="Toad")
|
|
242
|
+
server.serve()
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@main.command("about")
|
|
246
|
+
def about() -> None:
|
|
247
|
+
"""Show about information."""
|
|
248
|
+
|
|
249
|
+
from toad import about
|
|
250
|
+
|
|
251
|
+
app = ToadApp()
|
|
252
|
+
|
|
253
|
+
print(about.render(app))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
if __name__ == "__main__":
|
|
257
|
+
main()
|
toad/code_analyze.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from textual.highlight import highlight, guess_language
|
|
2
|
+
from pygments.util import ClassNotFound
|
|
3
|
+
from pygments.lexers import get_lexer_by_name, guess_lexer_for_filename
|
|
4
|
+
from pygments.token import Token
|
|
5
|
+
|
|
6
|
+
SPECIAL = {Token.Name.Function.Magic, Token.Name.Function, Token.Name.Class}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_special_name_from_code(code: str, language: str) -> list[str]:
|
|
10
|
+
try:
|
|
11
|
+
lexer = get_lexer_by_name(
|
|
12
|
+
language,
|
|
13
|
+
stripnl=False,
|
|
14
|
+
ensurenl=True,
|
|
15
|
+
tabsize=8,
|
|
16
|
+
)
|
|
17
|
+
except ClassNotFound:
|
|
18
|
+
lexer = get_lexer_by_name(
|
|
19
|
+
"text",
|
|
20
|
+
stripnl=False,
|
|
21
|
+
ensurenl=True,
|
|
22
|
+
tabsize=8,
|
|
23
|
+
)
|
|
24
|
+
special: list[str] = []
|
|
25
|
+
for token_type, token in lexer.get_tokens(code):
|
|
26
|
+
if token_type in SPECIAL:
|
|
27
|
+
special.append(token)
|
|
28
|
+
return special
|
toad/complete.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from typing import Iterable
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Complete:
|
|
6
|
+
"""Stores substrings and their potential completions."""
|
|
7
|
+
|
|
8
|
+
def __init__(self) -> None:
|
|
9
|
+
self._word_map: defaultdict[str, set[str]] = defaultdict(set)
|
|
10
|
+
|
|
11
|
+
def add_words(self, words: Iterable[str]) -> None:
|
|
12
|
+
"""Add word(s) word map.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
words: Iterable of words to add.
|
|
16
|
+
"""
|
|
17
|
+
word_map = self._word_map
|
|
18
|
+
for word in words:
|
|
19
|
+
for index in range(1, len(word)):
|
|
20
|
+
word_map[word[:index]].add(word[index:])
|
|
21
|
+
|
|
22
|
+
def __call__(self, word: str) -> list[str]:
|
|
23
|
+
return sorted(self._word_map.get(word, []), key=len)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
complete = Complete()
|
|
28
|
+
complete.add_words(["ls", "ls -al", "echo 'hello'"])
|
|
29
|
+
|
|
30
|
+
print(complete("l"))
|
|
31
|
+
|
|
32
|
+
from rich import print
|
|
33
|
+
|
|
34
|
+
print(complete._word_map)
|
toad/constants.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains constants, which may be set in environment variables.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Final
|
|
9
|
+
|
|
10
|
+
get_environ = os.environ.get
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_environ_bool(name: str, default: bool = False) -> bool:
|
|
14
|
+
"""Check an environment variable switch.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
name: Name of environment variable.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
`True` if the env var is "1", otherwise `False`.
|
|
21
|
+
"""
|
|
22
|
+
has_environ = get_environ(name, "1" if default else "0") == "1"
|
|
23
|
+
return has_environ
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_environ_int(
|
|
27
|
+
name: str, default: int, minimum: int | None = None, maximum: int | None = None
|
|
28
|
+
) -> int:
|
|
29
|
+
"""Retrieves an integer environment variable.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
name: Name of environment variable.
|
|
33
|
+
default: The value to use if the value is not set, or set to something other
|
|
34
|
+
than a valid integer.
|
|
35
|
+
minimum: Optional minimum value.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
The integer associated with the environment variable if it's set to a valid int
|
|
39
|
+
or the default value otherwise.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
value = int(os.environ[name])
|
|
43
|
+
except KeyError:
|
|
44
|
+
return default
|
|
45
|
+
except ValueError:
|
|
46
|
+
return default
|
|
47
|
+
if minimum is not None:
|
|
48
|
+
return max(minimum, value)
|
|
49
|
+
if maximum is not None:
|
|
50
|
+
return min(maximum, value)
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
ACP_INITIALIZE: Final[bool] = _get_environ_bool("TOAD_ACP_INITIALIZE", True)
|
|
55
|
+
"""Initialize ACP agents?"""
|
|
56
|
+
|
|
57
|
+
DEBUG: Final[bool] = _get_environ_bool("DEBUG", False)
|
|
58
|
+
"""Debug flag."""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from textual.widgets import Markdown
|
|
2
|
+
from textual.widgets._markdown import MarkdownBlock
|
|
3
|
+
from textual.content import Content
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ConversationCodeFence(Markdown.BLOCKS["fence"]):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
CUSTOM_BLOCKS = {"fence": ConversationCodeFence}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConversationMarkdown(Markdown):
|
|
14
|
+
"""Markdown widget with custom blocks."""
|
|
15
|
+
|
|
16
|
+
def get_block_class(self, block_name: str) -> type[MarkdownBlock]:
|
|
17
|
+
if (custom_block := CUSTOM_BLOCKS.get("block_name")) is not None:
|
|
18
|
+
return custom_block
|
|
19
|
+
return super().get_block_class(block_name)
|