autosh 0.0.5__tar.gz → 0.0.7__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.
- {autosh-0.0.5 → autosh-0.0.7}/PKG-INFO +14 -11
- {autosh-0.0.5 → autosh-0.0.7}/README.md +7 -7
- {autosh-0.0.5 → autosh-0.0.7}/autosh/config.py +8 -0
- {autosh-0.0.5 → autosh-0.0.7}/autosh/main.py +5 -3
- autosh-0.0.7/autosh/plugins/__init__.py +160 -0
- {autosh-0.0.5 → autosh-0.0.7}/autosh/plugins/calc.py +2 -6
- {autosh-0.0.5 → autosh-0.0.7}/autosh/plugins/cli.py +30 -19
- {autosh-0.0.5 → autosh-0.0.7}/autosh/plugins/clock.py +2 -2
- {autosh-0.0.5 → autosh-0.0.7}/autosh/plugins/code.py +7 -6
- {autosh-0.0.5 → autosh-0.0.7}/autosh/plugins/search.py +4 -16
- {autosh-0.0.5 → autosh-0.0.7}/autosh/plugins/web.py +2 -2
- {autosh-0.0.5 → autosh-0.0.7}/autosh/session.py +81 -40
- {autosh-0.0.5 → autosh-0.0.7}/pyproject.toml +19 -4
- autosh-0.0.5/autosh/plugins/__init__.py +0 -103
- {autosh-0.0.5 → autosh-0.0.7}/.gitignore +0 -0
- {autosh-0.0.5 → autosh-0.0.7}/LICENSE +0 -0
- {autosh-0.0.5 → autosh-0.0.7}/autosh/__init__.py +0 -0
- {autosh-0.0.5 → autosh-0.0.7}/autosh/config-template.toml +0 -0
@@ -1,10 +1,13 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: autosh
|
3
|
-
Version: 0.0.
|
4
|
-
Summary:
|
3
|
+
Version: 0.0.7
|
4
|
+
Summary: The AI-powered, noob-friendly interactive shell
|
5
|
+
Author-email: Wenyu Zhao <wenyuzhaox@gmail.com>
|
6
|
+
License-Expression: MIT
|
5
7
|
License-File: LICENSE
|
6
|
-
|
7
|
-
Requires-
|
8
|
+
Keywords: agent,chatgpt,cli,command line,gpt,interactive,llm,openai,openrouter,shell,terminal
|
9
|
+
Requires-Python: >=3.12
|
10
|
+
Requires-Dist: agentia>=0.0.8
|
8
11
|
Requires-Dist: asyncio>=3.4.3
|
9
12
|
Requires-Dist: markdownify>=1.1.0
|
10
13
|
Requires-Dist: neongrid>=0.0.1
|
@@ -34,7 +37,7 @@ As an interactive shell: `ash` (alternatively, `autosh`)
|
|
34
37
|
Execute a single prompt: `ash "list current directory"`
|
35
38
|
|
36
39
|
Process piped data:
|
37
|
-
* `cat README.md | ash -y "
|
40
|
+
* `cat README.md | ash -y "summarize"`
|
38
41
|
* `cat in.csv | ash -y -q "double the first numeric column" > out.csv`
|
39
42
|
|
40
43
|
## Scripting
|
@@ -53,15 +56,15 @@ First, please display a welcome message:)
|
|
53
56
|
Write "Hello, world" to _test.log
|
54
57
|
```
|
55
58
|
|
56
|
-
* Run the script: `ash simple.a.md` or `chmod +x simple.a.md && simple.a.md`
|
57
|
-
* Auto
|
59
|
+
* Run the script: `ash simple.a.md` or `chmod +x simple.a.md && ./simple.a.md`
|
60
|
+
* Auto-generate help messages:
|
58
61
|
|
59
62
|
```console
|
60
63
|
$ ash simple.a.md -h
|
61
64
|
|
62
65
|
Usage: simple.a.md [OPTIONS]
|
63
66
|
|
64
|
-
This is a simple file manipulation script that writes "Hello, world" to a log file named
|
67
|
+
This is a simple file manipulation script that writes "Hello, world" to a log file named _test.log.
|
65
68
|
|
66
69
|
Options:
|
67
70
|
|
@@ -70,9 +73,9 @@ Write "Hello, world" to _test.log
|
|
70
73
|
|
71
74
|
## Plugins
|
72
75
|
|
73
|
-
`autosh`
|
76
|
+
`autosh` comes with several plugins to expand its capabilities:
|
74
77
|
|
75
|
-
* `ash "Create a directory
|
78
|
+
* `ash "Create a directory 'my-news', list the latest news, and for each news item, put the summary in a separate markdown file in this directory"`
|
76
79
|
|
77
80
|
# TODO
|
78
81
|
|
@@ -80,4 +83,4 @@ Write "Hello, world" to _test.log
|
|
80
83
|
- [ ] RAG for non-text files
|
81
84
|
- [ ] Plugin system
|
82
85
|
- [ ] MCP support
|
83
|
-
- [
|
86
|
+
- [x] Improved input widget with history and auto-completion
|
@@ -15,7 +15,7 @@ As an interactive shell: `ash` (alternatively, `autosh`)
|
|
15
15
|
Execute a single prompt: `ash "list current directory"`
|
16
16
|
|
17
17
|
Process piped data:
|
18
|
-
* `cat README.md | ash -y "
|
18
|
+
* `cat README.md | ash -y "summarize"`
|
19
19
|
* `cat in.csv | ash -y -q "double the first numeric column" > out.csv`
|
20
20
|
|
21
21
|
## Scripting
|
@@ -34,15 +34,15 @@ First, please display a welcome message:)
|
|
34
34
|
Write "Hello, world" to _test.log
|
35
35
|
```
|
36
36
|
|
37
|
-
* Run the script: `ash simple.a.md` or `chmod +x simple.a.md && simple.a.md`
|
38
|
-
* Auto
|
37
|
+
* Run the script: `ash simple.a.md` or `chmod +x simple.a.md && ./simple.a.md`
|
38
|
+
* Auto-generate help messages:
|
39
39
|
|
40
40
|
```console
|
41
41
|
$ ash simple.a.md -h
|
42
42
|
|
43
43
|
Usage: simple.a.md [OPTIONS]
|
44
44
|
|
45
|
-
This is a simple file manipulation script that writes "Hello, world" to a log file named
|
45
|
+
This is a simple file manipulation script that writes "Hello, world" to a log file named _test.log.
|
46
46
|
|
47
47
|
Options:
|
48
48
|
|
@@ -51,9 +51,9 @@ Write "Hello, world" to _test.log
|
|
51
51
|
|
52
52
|
## Plugins
|
53
53
|
|
54
|
-
`autosh`
|
54
|
+
`autosh` comes with several plugins to expand its capabilities:
|
55
55
|
|
56
|
-
* `ash "Create a directory
|
56
|
+
* `ash "Create a directory 'my-news', list the latest news, and for each news item, put the summary in a separate markdown file in this directory"`
|
57
57
|
|
58
58
|
# TODO
|
59
59
|
|
@@ -61,4 +61,4 @@ Write "Hello, world" to _test.log
|
|
61
61
|
- [ ] RAG for non-text files
|
62
62
|
- [ ] Plugin system
|
63
63
|
- [ ] MCP support
|
64
|
-
- [
|
64
|
+
- [x] Improved input widget with history and auto-completion
|
@@ -35,6 +35,14 @@ class Config(BaseModel):
|
|
35
35
|
description="The LLM model to use for reasoning before executing commands",
|
36
36
|
)
|
37
37
|
api_key: str | None = Field(default=None, description="OpenRouter API key.")
|
38
|
+
repl_banner: str = Field(
|
39
|
+
default="🦄 Welcome to [cyan]autosh[/cyan]. The AI-powered, noob-friendly interactive shell.",
|
40
|
+
description="The banner for the REPL.",
|
41
|
+
)
|
42
|
+
repl_prompt: str = Field(
|
43
|
+
default="[bold on cyan]{short_cwd}[/bold on cyan][cyan]\ue0b0[/cyan] ",
|
44
|
+
description="The prompt for the REPL user input.",
|
45
|
+
)
|
38
46
|
|
39
47
|
plugins: Plugins = Field(
|
40
48
|
default_factory=Plugins,
|
@@ -32,9 +32,11 @@ async def start_session(prompt: str | None, args: list[str]):
|
|
32
32
|
os.environ["OPENROUTER_INCLUDE_REASONING"] = "false"
|
33
33
|
await session.init()
|
34
34
|
piped_stdin = not sys.stdin.isatty()
|
35
|
-
|
35
|
+
piped_stdout = not sys.stdout.isatty()
|
36
|
+
if (not CLI_OPTIONS.yes) and (piped_stdin or piped_stdout):
|
36
37
|
rich.print(
|
37
|
-
"[bold red]Error:[/bold red] [red]--yes is required when using piped stdin.[/red]"
|
38
|
+
"[bold red]Error:[/bold red] [red]--yes (-y) is required when using piped stdin or stdout.[/red]",
|
39
|
+
file=sys.stderr,
|
38
40
|
)
|
39
41
|
sys.exit(1)
|
40
42
|
if prompt:
|
@@ -121,7 +123,7 @@ def parse_args() -> tuple[str | None, list[str]]:
|
|
121
123
|
try:
|
122
124
|
args = p.parse_args()
|
123
125
|
except argparse.ArgumentError as e:
|
124
|
-
rich.print(f"[bold red]Error:[/bold red] {str(e)}")
|
126
|
+
rich.print(f"[bold red]Error:[/bold red] {str(e)}", file=sys.stderr)
|
125
127
|
print_help()
|
126
128
|
sys.exit(1)
|
127
129
|
|
@@ -0,0 +1,160 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
import sys
|
3
|
+
from typing import Any, Callable
|
4
|
+
import rich
|
5
|
+
from rich.prompt import Confirm
|
6
|
+
from rich.panel import Panel
|
7
|
+
from rich.console import RenderableType
|
8
|
+
from autosh.config import CLI_OPTIONS, CONFIG
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass
|
12
|
+
class Banner:
|
13
|
+
title: str | Callable[[Any], str]
|
14
|
+
|
15
|
+
text: str | Callable[[Any], str] | None = None
|
16
|
+
|
17
|
+
text_key: str | None = None
|
18
|
+
|
19
|
+
code: Callable[[Any], RenderableType] | None = None
|
20
|
+
"""
|
21
|
+
Turn the banner into a code block
|
22
|
+
"""
|
23
|
+
|
24
|
+
user_consent: bool = False
|
25
|
+
|
26
|
+
def __get_text(self, args: Any):
|
27
|
+
if self.text:
|
28
|
+
return self.text(args) if callable(self.text) else self.text
|
29
|
+
elif self.text_key:
|
30
|
+
return args.get(self.text_key)
|
31
|
+
return None
|
32
|
+
|
33
|
+
def __print_simple_banner(self, args: Any):
|
34
|
+
title = self.title(args) if callable(self.title) else self.title
|
35
|
+
if not sys.stdout.isatty():
|
36
|
+
s = f"[TOOL] {title}"
|
37
|
+
if text := self.__get_text(args):
|
38
|
+
s += f" {text}"
|
39
|
+
print(s)
|
40
|
+
else:
|
41
|
+
s = f"[bold on magenta] {title} [/bold on magenta]"
|
42
|
+
if text := self.__get_text(args):
|
43
|
+
s += f" [italic dim]{text}[/italic dim]"
|
44
|
+
rich.print(s)
|
45
|
+
|
46
|
+
def render(self, args: Any, prefix_newline: bool = True) -> bool:
|
47
|
+
if CLI_OPTIONS.quiet and not (self.user_consent and not CLI_OPTIONS.yes):
|
48
|
+
return False
|
49
|
+
if prefix_newline:
|
50
|
+
print()
|
51
|
+
if self.code:
|
52
|
+
code = self.code(args)
|
53
|
+
if CLI_OPTIONS.quiet and self.user_consent and not CLI_OPTIONS.yes:
|
54
|
+
self.__print_simple_banner(args)
|
55
|
+
return True
|
56
|
+
panel = Panel.fit(
|
57
|
+
code, title=f"[magenta]{self.title}[/magenta]", title_align="left"
|
58
|
+
)
|
59
|
+
rich.print(panel)
|
60
|
+
else:
|
61
|
+
self.__print_simple_banner(args)
|
62
|
+
return True
|
63
|
+
|
64
|
+
|
65
|
+
# def __print_simple_banner(tag: str, text: str | None = None):
|
66
|
+
# if CLI_OPTIONS.quiet:
|
67
|
+
# return
|
68
|
+
# if not sys.stdout.isatty():
|
69
|
+
# s = f"\n[TOOL] {tag}"
|
70
|
+
# if text:
|
71
|
+
# s += f" {text}"
|
72
|
+
# print(s)
|
73
|
+
# return
|
74
|
+
# s = f"\n[bold on magenta] {tag} [/bold on magenta]"
|
75
|
+
# if text:
|
76
|
+
# s += f" [italic dim]{text}[/italic dim]"
|
77
|
+
# rich.print(s)
|
78
|
+
|
79
|
+
|
80
|
+
# def simple_banner(
|
81
|
+
# tag: str | Callable[[Any], str],
|
82
|
+
# text: Callable[[Any], str] | None = None,
|
83
|
+
# text_key: str | None = None,
|
84
|
+
# ):
|
85
|
+
# return lambda x: __print_simple_banner(
|
86
|
+
# tag if isinstance(tag, str) else tag(x),
|
87
|
+
# text(x) if text else (x.get(text_key) if text_key else None),
|
88
|
+
# )
|
89
|
+
|
90
|
+
|
91
|
+
# def __print_code_preview_banner(
|
92
|
+
# title: str, content: RenderableType, short: str | None = None
|
93
|
+
# ):
|
94
|
+
# if CLI_OPTIONS.quiet:
|
95
|
+
# if short and not CLI_OPTIONS.yes:
|
96
|
+
# rich.print(f"\n[magenta]{short}[/magenta]\n")
|
97
|
+
# return
|
98
|
+
# panel = Panel.fit(content, title=f"[magenta]{title}[/magenta]", title_align="left")
|
99
|
+
# rich.print()
|
100
|
+
# rich.print(panel)
|
101
|
+
|
102
|
+
|
103
|
+
# def code_preview_banner(
|
104
|
+
# title: str | Callable[[Any], str],
|
105
|
+
# short: str | Callable[[Any], str],
|
106
|
+
# content: Callable[[Any], RenderableType],
|
107
|
+
# ):
|
108
|
+
# return lambda x: __print_code_preview_banner(
|
109
|
+
# title=title if isinstance(title, str) else title(x),
|
110
|
+
# content=content(x),
|
111
|
+
# short=short if isinstance(short, str) else short(x),
|
112
|
+
# )
|
113
|
+
|
114
|
+
|
115
|
+
def code_result_panel(
|
116
|
+
title: str,
|
117
|
+
out: str | None = None,
|
118
|
+
err: str | None = None,
|
119
|
+
):
|
120
|
+
if CLI_OPTIONS.quiet:
|
121
|
+
return
|
122
|
+
print()
|
123
|
+
if isinstance(out, str):
|
124
|
+
out = out.strip()
|
125
|
+
if isinstance(err, str):
|
126
|
+
err = err.strip()
|
127
|
+
if not out and not err:
|
128
|
+
rich.print(title)
|
129
|
+
else:
|
130
|
+
text = out if out else ""
|
131
|
+
text += (("\n---\n" if out else "") + err) if err else ""
|
132
|
+
panel = Panel.fit(text, title=title, title_align="left", style="dim")
|
133
|
+
rich.print(panel)
|
134
|
+
|
135
|
+
|
136
|
+
from . import calc
|
137
|
+
from . import clock
|
138
|
+
from . import code
|
139
|
+
from . import search
|
140
|
+
from . import web
|
141
|
+
from . import cli
|
142
|
+
|
143
|
+
|
144
|
+
def create_plugins():
|
145
|
+
"""Get all plugins in the autosh.plugins module."""
|
146
|
+
cfgs = CONFIG.plugins
|
147
|
+
plugins = []
|
148
|
+
if cfgs.calc is not None:
|
149
|
+
plugins.append(calc.CalculatorPlugin(cfgs.calc.model_dump()))
|
150
|
+
if cfgs.cli is not None:
|
151
|
+
plugins.append(cli.CLIPlugin(cfgs.cli.model_dump()))
|
152
|
+
if cfgs.clock is not None:
|
153
|
+
plugins.append(clock.ClockPlugin(cfgs.clock.model_dump()))
|
154
|
+
if cfgs.code is not None:
|
155
|
+
plugins.append(code.CodePlugin(cfgs.code.model_dump()))
|
156
|
+
if cfgs.search is not None:
|
157
|
+
plugins.append(search.SearchPlugin(cfgs.search.model_dump()))
|
158
|
+
if cfgs.web is not None:
|
159
|
+
plugins.append(web.WebPlugin(cfgs.web.model_dump()))
|
160
|
+
return plugins
|
@@ -1,16 +1,12 @@
|
|
1
1
|
from agentia.plugins import tool, Plugin
|
2
2
|
from typing import Annotated
|
3
|
-
from . import
|
3
|
+
from . import Banner
|
4
4
|
|
5
5
|
|
6
6
|
class CalculatorPlugin(Plugin):
|
7
7
|
NAME = "calc"
|
8
8
|
|
9
|
-
@tool(
|
10
|
-
metadata={
|
11
|
-
"banner": simple_banner("CALC", dim=lambda a: a.get("expression", ""))
|
12
|
-
}
|
13
|
-
)
|
9
|
+
@tool(metadata={"banner": Banner("CALC", text_key="expression")})
|
14
10
|
def evaluate(
|
15
11
|
self,
|
16
12
|
expression: Annotated[
|
@@ -8,7 +8,7 @@ from enum import StrEnum
|
|
8
8
|
|
9
9
|
from autosh.config import CLI_OPTIONS
|
10
10
|
|
11
|
-
from . import code_result_panel,
|
11
|
+
from . import code_result_panel, Banner
|
12
12
|
|
13
13
|
|
14
14
|
class Color(StrEnum):
|
@@ -59,7 +59,7 @@ class CLIPlugin(Plugin):
|
|
59
59
|
rich.print(text, file=sys.stderr if stderr else sys.stdout, end=end)
|
60
60
|
return "DONE. You can continue and no need to repeat the text"
|
61
61
|
|
62
|
-
@tool(metadata={"banner":
|
62
|
+
@tool(metadata={"banner": Banner("CWD", text_key="path")})
|
63
63
|
def chdir(self, path: Annotated[str, "The path to the new working directory"]):
|
64
64
|
"""
|
65
65
|
Changes the current working directory of the terminal to another directory.
|
@@ -67,8 +67,9 @@ class CLIPlugin(Plugin):
|
|
67
67
|
if not os.path.exists(path):
|
68
68
|
raise FileNotFoundError(f"Path `{path}` does not exist.")
|
69
69
|
os.chdir(path)
|
70
|
+
return f"DONE"
|
70
71
|
|
71
|
-
@tool(metadata={"banner":
|
72
|
+
@tool(metadata={"banner": Banner("GET ARGV")})
|
72
73
|
def get_argv(self):
|
73
74
|
"""
|
74
75
|
Get the command line arguments.
|
@@ -80,7 +81,7 @@ class CLIPlugin(Plugin):
|
|
80
81
|
"args": CLI_OPTIONS.args,
|
81
82
|
}
|
82
83
|
|
83
|
-
@tool(metadata={"banner":
|
84
|
+
@tool(metadata={"banner": Banner("GET ENV", text_key="key")})
|
84
85
|
def get_env(self, key: Annotated[str, "The environment variable to get"]):
|
85
86
|
"""
|
86
87
|
Get an environment variable.
|
@@ -89,7 +90,7 @@ class CLIPlugin(Plugin):
|
|
89
90
|
raise KeyError(f"Environment variable `{key}` does not exist.")
|
90
91
|
return os.environ[key]
|
91
92
|
|
92
|
-
@tool(metadata={"banner":
|
93
|
+
@tool(metadata={"banner": Banner("GET ALL ENVS")})
|
93
94
|
def get_all_envs(self):
|
94
95
|
"""
|
95
96
|
Get all environment variables.
|
@@ -101,10 +102,13 @@ class CLIPlugin(Plugin):
|
|
101
102
|
|
102
103
|
@tool(
|
103
104
|
metadata={
|
104
|
-
"banner":
|
105
|
-
|
106
|
-
text=lambda a:
|
107
|
-
|
105
|
+
"banner": Banner(
|
106
|
+
title=lambda a: "SET ENV" if a.get("value") else "DEL ENV",
|
107
|
+
text=lambda a: (
|
108
|
+
f"{a.get("key", "")} = {a.get("value", "")}"
|
109
|
+
if a.get("value")
|
110
|
+
else a.get("key", "")
|
111
|
+
),
|
108
112
|
),
|
109
113
|
}
|
110
114
|
)
|
@@ -126,7 +130,7 @@ class CLIPlugin(Plugin):
|
|
126
130
|
os.environ[key] = value
|
127
131
|
return f"DONE"
|
128
132
|
|
129
|
-
@tool(metadata={"banner":
|
133
|
+
@tool(metadata={"banner": Banner("READ", text_key="path")})
|
130
134
|
def read(
|
131
135
|
self,
|
132
136
|
path: Annotated[str, "The path to the file to read"],
|
@@ -144,10 +148,9 @@ class CLIPlugin(Plugin):
|
|
144
148
|
|
145
149
|
@tool(
|
146
150
|
metadata={
|
147
|
-
"banner":
|
148
|
-
|
149
|
-
text=lambda a: a.get("path", ""),
|
150
|
-
dim=lambda a: f"({len(a.get('content', ''))} bytes)",
|
151
|
+
"banner": Banner(
|
152
|
+
title=lambda a: "WRITE" if not a.get("append") else "APPEND",
|
153
|
+
text=lambda a: f"{a.get("path", "")} ({len(a.get('content', ''))} bytes)",
|
151
154
|
),
|
152
155
|
}
|
153
156
|
)
|
@@ -209,10 +212,11 @@ class CLIPlugin(Plugin):
|
|
209
212
|
|
210
213
|
@tool(
|
211
214
|
metadata={
|
212
|
-
"banner":
|
215
|
+
"banner": Banner(
|
213
216
|
title="Run Command",
|
214
|
-
|
215
|
-
|
217
|
+
text_key="command",
|
218
|
+
code=lambda a: f"[magenta][bold]➜[/bold] [italic]{a.get("command", "")}[/italic][/magenta]\n\n[dim]{a.get("explanation", "")}[/dim]",
|
219
|
+
user_consent=True,
|
216
220
|
)
|
217
221
|
}
|
218
222
|
)
|
@@ -263,9 +267,16 @@ class CLIPlugin(Plugin):
|
|
263
267
|
}
|
264
268
|
return result
|
265
269
|
|
266
|
-
@tool(metadata={"banner":
|
267
|
-
def exit(
|
270
|
+
@tool(metadata={"banner": Banner("EXIT")})
|
271
|
+
def exit(
|
272
|
+
self,
|
273
|
+
exitcode: Annotated[int, "The exit code of this shell session"] = 0,
|
274
|
+
reason: Annotated[str | None, "The reason for exiting"] = None,
|
275
|
+
):
|
268
276
|
"""
|
269
277
|
Exit the current shell session with an optional exit code.
|
270
278
|
"""
|
279
|
+
if reason and exitcode != 0:
|
280
|
+
rich.print(f"\n[bold red]ABORT: {reason}[/bold red]")
|
271
281
|
sys.exit(exitcode)
|
282
|
+
return f"EXITED with code {exitcode}"
|
@@ -2,11 +2,11 @@ import datetime
|
|
2
2
|
from agentia.plugins import tool, Plugin
|
3
3
|
import tzlocal
|
4
4
|
|
5
|
-
from autosh.plugins import
|
5
|
+
from autosh.plugins import Banner
|
6
6
|
|
7
7
|
|
8
8
|
class ClockPlugin(Plugin):
|
9
|
-
@tool(metadata={"banner":
|
9
|
+
@tool(metadata={"banner": Banner("GET TIME")})
|
10
10
|
def get_current_time(self):
|
11
11
|
"""Get the current UTC time in ISO format"""
|
12
12
|
utc = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
@@ -5,7 +5,7 @@ from rich.syntax import Syntax
|
|
5
5
|
from rich.console import group
|
6
6
|
from contextlib import redirect_stdout, redirect_stderr
|
7
7
|
import io
|
8
|
-
from . import
|
8
|
+
from . import Banner, code_result_panel
|
9
9
|
|
10
10
|
|
11
11
|
@group()
|
@@ -18,10 +18,10 @@ def code_with_explanation(code: str, explanation: str):
|
|
18
18
|
class CodePlugin(Plugin):
|
19
19
|
@tool(
|
20
20
|
metadata={
|
21
|
-
"banner":
|
22
|
-
title="Run Python",
|
23
|
-
|
24
|
-
|
21
|
+
"banner": Banner(
|
22
|
+
title="Run Python Code",
|
23
|
+
text_key="explanation",
|
24
|
+
code=lambda a: code_with_explanation(
|
25
25
|
a.get("python_code", ""), a.get("explanation", "")
|
26
26
|
),
|
27
27
|
)
|
@@ -31,7 +31,8 @@ class CodePlugin(Plugin):
|
|
31
31
|
self,
|
32
32
|
python_code: Annotated[str, "The python code to run."],
|
33
33
|
explanation: Annotated[
|
34
|
-
str,
|
34
|
+
str,
|
35
|
+
"Briefly explain what this code does, and what are you going to use it for.",
|
35
36
|
],
|
36
37
|
):
|
37
38
|
"""
|
@@ -3,7 +3,7 @@ from typing import Annotated, override
|
|
3
3
|
import os
|
4
4
|
from tavily import TavilyClient
|
5
5
|
|
6
|
-
from autosh.plugins import
|
6
|
+
from autosh.plugins import Banner
|
7
7
|
|
8
8
|
|
9
9
|
class SearchPlugin(Plugin):
|
@@ -17,11 +17,7 @@ class SearchPlugin(Plugin):
|
|
17
17
|
raise ValueError("Please set the TAVILY_API_KEY environment variable.")
|
18
18
|
self.__tavily = TavilyClient(api_key=key)
|
19
19
|
|
20
|
-
@tool(
|
21
|
-
metadata={
|
22
|
-
"banner": simple_banner("WEB SEARCH", dim=lambda a: a.get("query", "")),
|
23
|
-
}
|
24
|
-
)
|
20
|
+
@tool(metadata={"banner": Banner("WEB SEARCH", text_key="query")})
|
25
21
|
async def web_search(
|
26
22
|
self,
|
27
23
|
query: Annotated[
|
@@ -43,11 +39,7 @@ class SearchPlugin(Plugin):
|
|
43
39
|
)
|
44
40
|
return tavily_results
|
45
41
|
|
46
|
-
@tool(
|
47
|
-
metadata={
|
48
|
-
"banner": simple_banner("NEWS SEARCH", dim=lambda a: a.get("query", "")),
|
49
|
-
}
|
50
|
-
)
|
42
|
+
@tool(metadata={"banner": Banner("NEWS SEARCH", text_key="query")})
|
51
43
|
async def news_search(
|
52
44
|
self,
|
53
45
|
query: Annotated[
|
@@ -70,11 +62,7 @@ class SearchPlugin(Plugin):
|
|
70
62
|
)
|
71
63
|
return tavily_results
|
72
64
|
|
73
|
-
@tool(
|
74
|
-
metadata={
|
75
|
-
"banner": simple_banner("FINANCE SEARCH", dim=lambda a: a.get("query", ""))
|
76
|
-
}
|
77
|
-
)
|
65
|
+
@tool(metadata={"banner": Banner("FINANCE SEARCH", text_key="query")})
|
78
66
|
async def finance_search(
|
79
67
|
self,
|
80
68
|
query: Annotated[
|
@@ -7,7 +7,7 @@ from markdownify import markdownify
|
|
7
7
|
import uuid
|
8
8
|
from tavily import TavilyClient
|
9
9
|
|
10
|
-
from autosh.plugins import
|
10
|
+
from autosh.plugins import Banner
|
11
11
|
|
12
12
|
|
13
13
|
class WebPlugin(Plugin):
|
@@ -47,7 +47,7 @@ class WebPlugin(Plugin):
|
|
47
47
|
md = markdownify(res.text)
|
48
48
|
return {"content": md}
|
49
49
|
|
50
|
-
@tool(metadata={"banner":
|
50
|
+
@tool(metadata={"banner": Banner("BROWSE", text_key="url")})
|
51
51
|
def get_webpage_content(
|
52
52
|
self,
|
53
53
|
url: Annotated[str, "The URL of the web page to get the content of"],
|
@@ -1,5 +1,6 @@
|
|
1
|
-
import
|
1
|
+
from io import StringIO
|
2
2
|
from pathlib import Path
|
3
|
+
import socket
|
3
4
|
import sys
|
4
5
|
from agentia import (
|
5
6
|
Agent,
|
@@ -7,7 +8,7 @@ from agentia import (
|
|
7
8
|
Event,
|
8
9
|
ToolCallEvent,
|
9
10
|
MessageStream,
|
10
|
-
|
11
|
+
Run,
|
11
12
|
UserConsentEvent,
|
12
13
|
)
|
13
14
|
from agentia.plugins import PluginInitError
|
@@ -15,10 +16,11 @@ from neongrid.loading import Loading
|
|
15
16
|
|
16
17
|
from autosh.config import CLI_OPTIONS, CONFIG
|
17
18
|
import neongrid as ng
|
18
|
-
from .plugins import create_plugins
|
19
|
+
from .plugins import Banner, create_plugins
|
19
20
|
import rich
|
20
21
|
import platform
|
21
22
|
from rich.prompt import Confirm
|
23
|
+
import os
|
22
24
|
|
23
25
|
|
24
26
|
INSTRUCTIONS = f"""
|
@@ -87,8 +89,8 @@ class Session:
|
|
87
89
|
""",
|
88
90
|
)
|
89
91
|
agent.history.add(self.__get_argv_message())
|
90
|
-
|
91
|
-
async for stream in
|
92
|
+
run = agent.run(prompt, stream=True)
|
93
|
+
async for stream in run:
|
92
94
|
await self.__render_streamed_markdown(stream)
|
93
95
|
sys.exit(0)
|
94
96
|
|
@@ -117,7 +119,7 @@ class Session:
|
|
117
119
|
):
|
118
120
|
await self._print_help_and_exit(prompt)
|
119
121
|
# Execute the prompt
|
120
|
-
loading = self.__create_loading_indicator()
|
122
|
+
loading = self.__create_loading_indicator() if sys.stdout.isatty() else None
|
121
123
|
CLI_OPTIONS.prompt = prompt
|
122
124
|
self.agent.history.add(self.__get_argv_message())
|
123
125
|
if CLI_OPTIONS.stdin_has_data():
|
@@ -141,8 +143,8 @@ class Session:
|
|
141
143
|
role="user",
|
142
144
|
)
|
143
145
|
)
|
144
|
-
|
145
|
-
await self.
|
146
|
+
run = self.agent.run(prompt, stream=True, events=True)
|
147
|
+
await self.__process_run(run, loading, False)
|
146
148
|
|
147
149
|
async def exec_from_stdin(self):
|
148
150
|
if sys.stdin.isatty():
|
@@ -159,55 +161,94 @@ class Session:
|
|
159
161
|
prompt = f.read()
|
160
162
|
await self.exec_prompt(prompt)
|
161
163
|
|
162
|
-
async def __process_event(self, e: Event):
|
164
|
+
async def __process_event(self, e: Event, first: bool, repl: bool):
|
165
|
+
prefix_newline = repl or not first
|
163
166
|
if isinstance(e, UserConsentEvent):
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
if CLI_OPTIONS.yes:
|
167
|
+
if CLI_OPTIONS.yes:
|
168
|
+
e.response = True
|
169
|
+
return False
|
170
|
+
if prefix_newline:
|
171
|
+
print()
|
172
|
+
e.response = ng.confirm(e.message)
|
171
173
|
return True
|
172
|
-
result
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
return
|
174
|
+
if isinstance(e, ToolCallEvent) and e.result is None:
|
175
|
+
if (banner := (e.metadata or {}).get("banner")) and isinstance(
|
176
|
+
banner, Banner
|
177
|
+
):
|
178
|
+
return banner.render(e.arguments, prefix_newline=prefix_newline)
|
179
|
+
return False
|
178
180
|
|
179
|
-
async def
|
180
|
-
self,
|
181
|
+
async def __process_run(
|
182
|
+
self, run: Run[Event | MessageStream], loading: Loading | None, repl: bool
|
181
183
|
):
|
182
|
-
|
183
|
-
|
184
|
+
first = True
|
185
|
+
async for e in run:
|
186
|
+
if loading:
|
187
|
+
await loading.finish()
|
184
188
|
|
185
|
-
if isinstance(
|
186
|
-
await self.__process_event(
|
189
|
+
if isinstance(e, Event):
|
190
|
+
if await self.__process_event(e, first=first, repl=repl):
|
191
|
+
first = False
|
187
192
|
else:
|
188
|
-
|
189
|
-
|
190
|
-
|
193
|
+
if repl or not first:
|
194
|
+
print()
|
195
|
+
await self.__render_streamed_markdown(e)
|
196
|
+
first = False
|
191
197
|
|
192
|
-
loading
|
198
|
+
if loading:
|
199
|
+
loading = self.__create_loading_indicator()
|
193
200
|
|
194
|
-
|
201
|
+
if loading:
|
202
|
+
await loading.finish()
|
203
|
+
|
204
|
+
def __get_input_prompt(self):
|
205
|
+
cwd = Path.cwd()
|
206
|
+
relative_to_home = False
|
207
|
+
if cwd.is_relative_to(Path.home()):
|
208
|
+
cwd = cwd.relative_to(Path.home())
|
209
|
+
relative_to_home = True
|
210
|
+
# short cwd
|
211
|
+
short_cwd = "/" if not relative_to_home else "~/"
|
212
|
+
parts = []
|
213
|
+
for i, p in enumerate(cwd.parts):
|
214
|
+
if i == 0 and p == "/":
|
215
|
+
continue
|
216
|
+
if i != len(cwd.parts) - 1:
|
217
|
+
parts.append(p[0])
|
218
|
+
else:
|
219
|
+
parts.append(p)
|
220
|
+
short_cwd += "/".join(parts)
|
221
|
+
cwd = str(cwd) if not relative_to_home else "~/" + str(cwd)
|
222
|
+
host = socket.gethostname()
|
223
|
+
user = os.getlogin()
|
224
|
+
prompt = CONFIG.repl_prompt.format(
|
225
|
+
cwd=cwd,
|
226
|
+
short_cwd=short_cwd,
|
227
|
+
host=host,
|
228
|
+
user=user,
|
229
|
+
)
|
230
|
+
return prompt
|
195
231
|
|
196
232
|
async def run_repl(self):
|
233
|
+
if CONFIG.repl_banner:
|
234
|
+
rich.print(CONFIG.repl_banner)
|
235
|
+
first = True
|
197
236
|
while True:
|
198
237
|
try:
|
199
|
-
|
200
|
-
|
201
|
-
|
238
|
+
if not first:
|
239
|
+
print()
|
240
|
+
first = False
|
241
|
+
input_prompt = self.__get_input_prompt()
|
242
|
+
rich.print(input_prompt, end="", flush=True)
|
243
|
+
prompt = await ng.input("", sync=False, persist="/tmp/autosh-history")
|
244
|
+
prompt = prompt.strip()
|
202
245
|
if prompt in ["exit", "quit"]:
|
203
246
|
break
|
204
247
|
if len(prompt) == 0:
|
205
248
|
continue
|
206
249
|
loading = self.__create_loading_indicator()
|
207
|
-
|
208
|
-
|
209
|
-
)
|
210
|
-
await self.__process_completion(completion, loading)
|
250
|
+
run = self.agent.run(prompt, stream=True, events=True)
|
251
|
+
await self.__process_run(run, loading, True)
|
211
252
|
except KeyboardInterrupt:
|
212
253
|
break
|
213
254
|
|
@@ -1,9 +1,24 @@
|
|
1
1
|
[project]
|
2
2
|
name = "autosh"
|
3
|
-
version = "0.0.
|
4
|
-
description = "
|
3
|
+
version = "0.0.7"
|
4
|
+
description = "The AI-powered, noob-friendly interactive shell"
|
5
|
+
authors = [{ name = "Wenyu Zhao", email = "wenyuzhaox@gmail.com" }]
|
6
|
+
requires-python = ">=3.12"
|
5
7
|
readme = "README.md"
|
6
|
-
|
8
|
+
license = "MIT"
|
9
|
+
keywords = [
|
10
|
+
"terminal",
|
11
|
+
"shell",
|
12
|
+
"command line",
|
13
|
+
"cli",
|
14
|
+
"interactive",
|
15
|
+
"chatgpt",
|
16
|
+
"gpt",
|
17
|
+
"llm",
|
18
|
+
"agent",
|
19
|
+
"openai",
|
20
|
+
"openrouter",
|
21
|
+
]
|
7
22
|
dependencies = [
|
8
23
|
"asyncio>=3.4.3",
|
9
24
|
"markdownify>=1.1.0",
|
@@ -15,7 +30,7 @@ dependencies = [
|
|
15
30
|
"typer>=0.12.5",
|
16
31
|
"tzlocal>=5.3.1",
|
17
32
|
"neongrid>=0.0.1",
|
18
|
-
"agentia>=0.0.
|
33
|
+
"agentia>=0.0.8",
|
19
34
|
]
|
20
35
|
|
21
36
|
[project.scripts]
|
@@ -1,103 +0,0 @@
|
|
1
|
-
from typing import Any, Callable
|
2
|
-
import rich
|
3
|
-
from rich.prompt import Confirm
|
4
|
-
from rich.panel import Panel
|
5
|
-
from rich.console import RenderableType
|
6
|
-
from autosh.config import CLI_OPTIONS, CONFIG
|
7
|
-
|
8
|
-
|
9
|
-
def __print_simple_banner(tag: str, text: str | None = None, dim: str | None = None):
|
10
|
-
if CLI_OPTIONS.quiet:
|
11
|
-
return
|
12
|
-
s = f"\n[bold on magenta] {tag} [/bold on magenta]"
|
13
|
-
if text:
|
14
|
-
s += f" [italic magenta]{text}[/italic magenta]"
|
15
|
-
if dim:
|
16
|
-
s += f" [italic dim]{dim}[/italic dim]"
|
17
|
-
rich.print(s)
|
18
|
-
|
19
|
-
|
20
|
-
def simple_banner(
|
21
|
-
tag: str | Callable[[Any], str],
|
22
|
-
text: Callable[[Any], str] | None = None,
|
23
|
-
dim: Callable[[Any], str] | None = None,
|
24
|
-
):
|
25
|
-
return lambda x: __print_simple_banner(
|
26
|
-
tag if isinstance(tag, str) else tag(x),
|
27
|
-
text(x) if text else None,
|
28
|
-
dim(x) if dim else None,
|
29
|
-
)
|
30
|
-
|
31
|
-
|
32
|
-
def __print_code_preview_banner(
|
33
|
-
title: str, content: RenderableType, short: str | None = None
|
34
|
-
):
|
35
|
-
if CLI_OPTIONS.quiet:
|
36
|
-
if short and not CLI_OPTIONS.yes:
|
37
|
-
rich.print(f"\n[magenta]{short}[/magenta]\n")
|
38
|
-
return
|
39
|
-
panel = Panel.fit(content, title=f"[magenta]{title}[/magenta]", title_align="left")
|
40
|
-
rich.print()
|
41
|
-
rich.print(panel)
|
42
|
-
rich.print()
|
43
|
-
|
44
|
-
|
45
|
-
def code_preview_banner(
|
46
|
-
title: str | Callable[[Any], str],
|
47
|
-
short: str | Callable[[Any], str],
|
48
|
-
content: Callable[[Any], RenderableType],
|
49
|
-
):
|
50
|
-
return lambda x: __print_code_preview_banner(
|
51
|
-
title=title if isinstance(title, str) else title(x),
|
52
|
-
content=content(x),
|
53
|
-
short=short if isinstance(short, str) else short(x),
|
54
|
-
)
|
55
|
-
|
56
|
-
|
57
|
-
def code_result_panel(
|
58
|
-
title: str,
|
59
|
-
out: str | None = None,
|
60
|
-
err: str | None = None,
|
61
|
-
):
|
62
|
-
if CLI_OPTIONS.quiet:
|
63
|
-
return
|
64
|
-
if isinstance(out, str):
|
65
|
-
out = out.strip()
|
66
|
-
if isinstance(err, str):
|
67
|
-
err = err.strip()
|
68
|
-
if not out and not err:
|
69
|
-
rich.print(title)
|
70
|
-
else:
|
71
|
-
text = out if out else ""
|
72
|
-
text += (("\n---\n" if out else "") + err) if err else ""
|
73
|
-
panel = Panel.fit(text, title=title, title_align="left", style="dim")
|
74
|
-
rich.print(panel)
|
75
|
-
if not CLI_OPTIONS.quiet:
|
76
|
-
rich.print()
|
77
|
-
|
78
|
-
|
79
|
-
from . import calc
|
80
|
-
from . import clock
|
81
|
-
from . import code
|
82
|
-
from . import search
|
83
|
-
from . import web
|
84
|
-
from . import cli
|
85
|
-
|
86
|
-
|
87
|
-
def create_plugins():
|
88
|
-
"""Get all plugins in the autosh.plugins module."""
|
89
|
-
cfgs = CONFIG.plugins
|
90
|
-
plugins = []
|
91
|
-
if cfgs.calc is not None:
|
92
|
-
plugins.append(calc.CalculatorPlugin(cfgs.calc.model_dump()))
|
93
|
-
if cfgs.cli is not None:
|
94
|
-
plugins.append(cli.CLIPlugin(cfgs.cli.model_dump()))
|
95
|
-
if cfgs.clock is not None:
|
96
|
-
plugins.append(clock.ClockPlugin(cfgs.clock.model_dump()))
|
97
|
-
if cfgs.code is not None:
|
98
|
-
plugins.append(code.CodePlugin(cfgs.code.model_dump()))
|
99
|
-
if cfgs.search is not None:
|
100
|
-
plugins.append(search.SearchPlugin(cfgs.search.model_dump()))
|
101
|
-
if cfgs.web is not None:
|
102
|
-
plugins.append(web.WebPlugin(cfgs.web.model_dump()))
|
103
|
-
return plugins
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|