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.
Files changed (120) hide show
  1. batrachian_toad-0.5.22.dist-info/METADATA +197 -0
  2. batrachian_toad-0.5.22.dist-info/RECORD +120 -0
  3. batrachian_toad-0.5.22.dist-info/WHEEL +4 -0
  4. batrachian_toad-0.5.22.dist-info/entry_points.txt +2 -0
  5. batrachian_toad-0.5.22.dist-info/licenses/LICENSE +661 -0
  6. toad/__init__.py +46 -0
  7. toad/__main__.py +4 -0
  8. toad/_loop.py +86 -0
  9. toad/about.py +90 -0
  10. toad/acp/agent.py +671 -0
  11. toad/acp/api.py +47 -0
  12. toad/acp/encode_tool_call_id.py +12 -0
  13. toad/acp/messages.py +138 -0
  14. toad/acp/prompt.py +54 -0
  15. toad/acp/protocol.py +426 -0
  16. toad/agent.py +62 -0
  17. toad/agent_schema.py +70 -0
  18. toad/agents.py +45 -0
  19. toad/ansi/__init__.py +1 -0
  20. toad/ansi/_ansi.py +1612 -0
  21. toad/ansi/_ansi_colors.py +264 -0
  22. toad/ansi/_control_codes.py +37 -0
  23. toad/ansi/_keys.py +251 -0
  24. toad/ansi/_sgr_styles.py +64 -0
  25. toad/ansi/_stream_parser.py +418 -0
  26. toad/answer.py +22 -0
  27. toad/app.py +557 -0
  28. toad/atomic.py +37 -0
  29. toad/cli.py +257 -0
  30. toad/code_analyze.py +28 -0
  31. toad/complete.py +34 -0
  32. toad/constants.py +58 -0
  33. toad/conversation_markdown.py +19 -0
  34. toad/danger.py +371 -0
  35. toad/data/agents/ampcode.com.toml +51 -0
  36. toad/data/agents/augmentcode.com.toml +40 -0
  37. toad/data/agents/claude.com.toml +41 -0
  38. toad/data/agents/docker.com.toml +59 -0
  39. toad/data/agents/geminicli.com.toml +28 -0
  40. toad/data/agents/goose.ai.toml +51 -0
  41. toad/data/agents/inference.huggingface.co.toml +33 -0
  42. toad/data/agents/kimi.com.toml +35 -0
  43. toad/data/agents/openai.com.toml +53 -0
  44. toad/data/agents/opencode.ai.toml +61 -0
  45. toad/data/agents/openhands.dev.toml +44 -0
  46. toad/data/agents/stakpak.dev.toml +61 -0
  47. toad/data/agents/vibe.mistral.ai.toml +27 -0
  48. toad/data/agents/vtcode.dev.toml +62 -0
  49. toad/data/images/frog.png +0 -0
  50. toad/data/sounds/turn-over.wav +0 -0
  51. toad/db.py +5 -0
  52. toad/dec.py +332 -0
  53. toad/directory.py +234 -0
  54. toad/directory_watcher.py +96 -0
  55. toad/fuzzy.py +140 -0
  56. toad/gist.py +2 -0
  57. toad/history.py +138 -0
  58. toad/jsonrpc.py +576 -0
  59. toad/menus.py +14 -0
  60. toad/messages.py +74 -0
  61. toad/option_content.py +51 -0
  62. toad/os.py +0 -0
  63. toad/path_complete.py +145 -0
  64. toad/path_filter.py +124 -0
  65. toad/paths.py +71 -0
  66. toad/pill.py +23 -0
  67. toad/prompt/extract.py +19 -0
  68. toad/prompt/resource.py +68 -0
  69. toad/protocol.py +28 -0
  70. toad/screens/action_modal.py +94 -0
  71. toad/screens/agent_modal.py +172 -0
  72. toad/screens/command_edit_modal.py +58 -0
  73. toad/screens/main.py +192 -0
  74. toad/screens/permissions.py +390 -0
  75. toad/screens/permissions.tcss +72 -0
  76. toad/screens/settings.py +254 -0
  77. toad/screens/settings.tcss +101 -0
  78. toad/screens/store.py +476 -0
  79. toad/screens/store.tcss +261 -0
  80. toad/settings.py +354 -0
  81. toad/settings_schema.py +318 -0
  82. toad/shell.py +263 -0
  83. toad/shell_read.py +42 -0
  84. toad/slash_command.py +34 -0
  85. toad/toad.tcss +752 -0
  86. toad/version.py +80 -0
  87. toad/visuals/columns.py +273 -0
  88. toad/widgets/agent_response.py +79 -0
  89. toad/widgets/agent_thought.py +41 -0
  90. toad/widgets/command_pane.py +224 -0
  91. toad/widgets/condensed_path.py +93 -0
  92. toad/widgets/conversation.py +1626 -0
  93. toad/widgets/danger_warning.py +65 -0
  94. toad/widgets/diff_view.py +709 -0
  95. toad/widgets/flash.py +81 -0
  96. toad/widgets/future_text.py +126 -0
  97. toad/widgets/grid_select.py +223 -0
  98. toad/widgets/highlighted_textarea.py +180 -0
  99. toad/widgets/mandelbrot.py +294 -0
  100. toad/widgets/markdown_note.py +13 -0
  101. toad/widgets/menu.py +147 -0
  102. toad/widgets/non_selectable_label.py +5 -0
  103. toad/widgets/note.py +18 -0
  104. toad/widgets/path_search.py +381 -0
  105. toad/widgets/plan.py +180 -0
  106. toad/widgets/project_directory_tree.py +74 -0
  107. toad/widgets/prompt.py +741 -0
  108. toad/widgets/question.py +337 -0
  109. toad/widgets/shell_result.py +35 -0
  110. toad/widgets/shell_terminal.py +18 -0
  111. toad/widgets/side_bar.py +74 -0
  112. toad/widgets/slash_complete.py +211 -0
  113. toad/widgets/strike_text.py +66 -0
  114. toad/widgets/terminal.py +526 -0
  115. toad/widgets/terminal_tool.py +338 -0
  116. toad/widgets/throbber.py +90 -0
  117. toad/widgets/tool_call.py +303 -0
  118. toad/widgets/user_input.py +23 -0
  119. toad/widgets/version.py +5 -0
  120. 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)