autosh 0.0.6__py3-none-any.whl → 0.0.8__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.
- autosh/config.py +9 -0
- autosh/main.py +20 -3
- autosh/plugins/__init__.py +48 -40
- autosh/plugins/calc.py +2 -6
- autosh/plugins/cli.py +28 -19
- autosh/plugins/clock.py +2 -2
- autosh/plugins/code.py +7 -6
- autosh/plugins/search.py +4 -16
- autosh/plugins/web.py +2 -2
- autosh/session.py +81 -42
- {autosh-0.0.6.dist-info → autosh-0.0.8.dist-info}/METADATA +9 -9
- autosh-0.0.8.dist-info/RECORD +17 -0
- autosh-0.0.6.dist-info/RECORD +0 -17
- {autosh-0.0.6.dist-info → autosh-0.0.8.dist-info}/WHEEL +0 -0
- {autosh-0.0.6.dist-info → autosh-0.0.8.dist-info}/entry_points.txt +0 -0
- {autosh-0.0.6.dist-info → autosh-0.0.8.dist-info}/licenses/LICENSE +0 -0
autosh/config.py
CHANGED
@@ -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,
|
@@ -69,6 +77,7 @@ class CLIOptions(BaseModel):
|
|
69
77
|
yes: bool = False
|
70
78
|
quiet: bool = False
|
71
79
|
think: bool = False
|
80
|
+
start_repl_after_prompt: bool = False
|
72
81
|
|
73
82
|
prompt: str | None = None
|
74
83
|
"""The prompt to execute"""
|
autosh/main.py
CHANGED
@@ -32,11 +32,21 @@ 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)
|
42
|
+
if CLI_OPTIONS.start_repl_after_prompt:
|
43
|
+
if piped_stdin or piped_stdout:
|
44
|
+
rich.print(
|
45
|
+
"[bold red]Error:[/bold red] [red]--repl is only available when not using piped stdin or stdout.[/red]",
|
46
|
+
file=sys.stderr,
|
47
|
+
)
|
48
|
+
sys.exit(1)
|
49
|
+
|
40
50
|
if prompt:
|
41
51
|
# No piped stdin, just run the prompt
|
42
52
|
if Path(prompt).is_file():
|
@@ -71,6 +81,11 @@ def print_help():
|
|
71
81
|
f"The LLM model to use. [dim]Default: {CONFIG.model} ({CONFIG.think_model} for reasoning).[/dim]",
|
72
82
|
],
|
73
83
|
["--think", "", "Use the reasoning models to think more before operating."],
|
84
|
+
[
|
85
|
+
"--repl",
|
86
|
+
"",
|
87
|
+
"Start a REPL session after executing the prompt or the script.",
|
88
|
+
],
|
74
89
|
["--help", "-h", "Show this message and exit."],
|
75
90
|
]
|
76
91
|
|
@@ -115,13 +130,14 @@ def parse_args() -> tuple[str | None, list[str]]:
|
|
115
130
|
p.add_argument("--quiet", "-q", action="store_true")
|
116
131
|
p.add_argument("--think", action="store_true")
|
117
132
|
p.add_argument("--model", "-m", type=str, default=None)
|
133
|
+
p.add_argument("--repl", action="store_true")
|
118
134
|
p.add_argument("PROMPT_OR_FILE", nargs="?", default=None)
|
119
135
|
p.add_argument("ARGS", nargs=argparse.REMAINDER)
|
120
136
|
|
121
137
|
try:
|
122
138
|
args = p.parse_args()
|
123
139
|
except argparse.ArgumentError as e:
|
124
|
-
rich.print(f"[bold red]Error:[/bold red] {str(e)}")
|
140
|
+
rich.print(f"[bold red]Error:[/bold red] {str(e)}", file=sys.stderr)
|
125
141
|
print_help()
|
126
142
|
sys.exit(1)
|
127
143
|
|
@@ -131,6 +147,7 @@ def parse_args() -> tuple[str | None, list[str]]:
|
|
131
147
|
|
132
148
|
CLI_OPTIONS.yes = args.yes
|
133
149
|
CLI_OPTIONS.quiet = args.quiet
|
150
|
+
CLI_OPTIONS.start_repl_after_prompt = args.repl
|
134
151
|
|
135
152
|
if args.model:
|
136
153
|
if args.think:
|
autosh/plugins/__init__.py
CHANGED
@@ -1,56 +1,64 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
import sys
|
1
3
|
from typing import Any, Callable
|
2
4
|
import rich
|
3
|
-
from rich.prompt import Confirm
|
4
5
|
from rich.panel import Panel
|
5
6
|
from rich.console import RenderableType
|
6
7
|
from autosh.config import CLI_OPTIONS, CONFIG
|
7
8
|
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
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)
|
10
|
+
@dataclass
|
11
|
+
class Banner:
|
12
|
+
title: str | Callable[[Any], str]
|
18
13
|
|
14
|
+
text: str | Callable[[Any], str] | None = None
|
19
15
|
|
20
|
-
|
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
|
-
)
|
16
|
+
text_key: str | None = None
|
30
17
|
|
18
|
+
code: Callable[[Any], RenderableType] | None = None
|
19
|
+
"""
|
20
|
+
Turn the banner into a code block
|
21
|
+
"""
|
31
22
|
|
32
|
-
|
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)
|
23
|
+
user_consent: bool = False
|
42
24
|
|
25
|
+
def __get_text(self, args: Any):
|
26
|
+
if self.text:
|
27
|
+
return self.text(args) if callable(self.text) else self.text
|
28
|
+
elif self.text_key:
|
29
|
+
return args.get(self.text_key)
|
30
|
+
return None
|
43
31
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
):
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
32
|
+
def __print_simple_banner(self, args: Any):
|
33
|
+
title = self.title(args) if callable(self.title) else self.title
|
34
|
+
if not sys.stdout.isatty():
|
35
|
+
s = f"[TOOL] {title}"
|
36
|
+
if text := self.__get_text(args):
|
37
|
+
s += f" {text}"
|
38
|
+
print(s)
|
39
|
+
else:
|
40
|
+
s = f"[bold on magenta] {title} [/bold on magenta]"
|
41
|
+
if text := self.__get_text(args):
|
42
|
+
s += f" [italic dim]{text}[/italic dim]"
|
43
|
+
rich.print(s)
|
44
|
+
|
45
|
+
def render(self, args: Any, prefix_newline: bool = True) -> bool:
|
46
|
+
if CLI_OPTIONS.quiet and not (self.user_consent and not CLI_OPTIONS.yes):
|
47
|
+
return False
|
48
|
+
if prefix_newline:
|
49
|
+
print()
|
50
|
+
if self.code:
|
51
|
+
code = self.code(args)
|
52
|
+
if CLI_OPTIONS.quiet and self.user_consent and not CLI_OPTIONS.yes:
|
53
|
+
self.__print_simple_banner(args)
|
54
|
+
return True
|
55
|
+
panel = Panel.fit(
|
56
|
+
code, title=f"[magenta]{self.title}[/magenta]", title_align="left"
|
57
|
+
)
|
58
|
+
rich.print(panel)
|
59
|
+
else:
|
60
|
+
self.__print_simple_banner(args)
|
61
|
+
return True
|
54
62
|
|
55
63
|
|
56
64
|
def code_result_panel(
|
autosh/plugins/calc.py
CHANGED
@@ -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[
|
autosh/plugins/cli.py
CHANGED
@@ -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.
|
@@ -69,7 +69,7 @@ class CLIPlugin(Plugin):
|
|
69
69
|
os.chdir(path)
|
70
70
|
return f"DONE"
|
71
71
|
|
72
|
-
@tool(metadata={"banner":
|
72
|
+
@tool(metadata={"banner": Banner("GET ARGV")})
|
73
73
|
def get_argv(self):
|
74
74
|
"""
|
75
75
|
Get the command line arguments.
|
@@ -81,7 +81,7 @@ class CLIPlugin(Plugin):
|
|
81
81
|
"args": CLI_OPTIONS.args,
|
82
82
|
}
|
83
83
|
|
84
|
-
@tool(metadata={"banner":
|
84
|
+
@tool(metadata={"banner": Banner("GET ENV", text_key="key")})
|
85
85
|
def get_env(self, key: Annotated[str, "The environment variable to get"]):
|
86
86
|
"""
|
87
87
|
Get an environment variable.
|
@@ -90,7 +90,7 @@ class CLIPlugin(Plugin):
|
|
90
90
|
raise KeyError(f"Environment variable `{key}` does not exist.")
|
91
91
|
return os.environ[key]
|
92
92
|
|
93
|
-
@tool(metadata={"banner":
|
93
|
+
@tool(metadata={"banner": Banner("GET ALL ENVS")})
|
94
94
|
def get_all_envs(self):
|
95
95
|
"""
|
96
96
|
Get all environment variables.
|
@@ -102,10 +102,13 @@ class CLIPlugin(Plugin):
|
|
102
102
|
|
103
103
|
@tool(
|
104
104
|
metadata={
|
105
|
-
"banner":
|
106
|
-
|
107
|
-
text=lambda a:
|
108
|
-
|
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
|
+
),
|
109
112
|
),
|
110
113
|
}
|
111
114
|
)
|
@@ -127,7 +130,7 @@ class CLIPlugin(Plugin):
|
|
127
130
|
os.environ[key] = value
|
128
131
|
return f"DONE"
|
129
132
|
|
130
|
-
@tool(metadata={"banner":
|
133
|
+
@tool(metadata={"banner": Banner("READ", text_key="path")})
|
131
134
|
def read(
|
132
135
|
self,
|
133
136
|
path: Annotated[str, "The path to the file to read"],
|
@@ -145,10 +148,9 @@ class CLIPlugin(Plugin):
|
|
145
148
|
|
146
149
|
@tool(
|
147
150
|
metadata={
|
148
|
-
"banner":
|
149
|
-
|
150
|
-
text=lambda a: a.get("path", ""),
|
151
|
-
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)",
|
152
154
|
),
|
153
155
|
}
|
154
156
|
)
|
@@ -210,10 +212,11 @@ class CLIPlugin(Plugin):
|
|
210
212
|
|
211
213
|
@tool(
|
212
214
|
metadata={
|
213
|
-
"banner":
|
215
|
+
"banner": Banner(
|
214
216
|
title="Run Command",
|
215
|
-
|
216
|
-
|
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,
|
217
220
|
)
|
218
221
|
}
|
219
222
|
)
|
@@ -264,10 +267,16 @@ class CLIPlugin(Plugin):
|
|
264
267
|
}
|
265
268
|
return result
|
266
269
|
|
267
|
-
@tool(metadata={"banner":
|
268
|
-
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
|
+
):
|
269
276
|
"""
|
270
277
|
Exit the current shell session with an optional exit code.
|
271
278
|
"""
|
279
|
+
if reason and exitcode != 0:
|
280
|
+
rich.print(f"\n[bold red]ABORT: {reason}[/bold red]")
|
272
281
|
sys.exit(exitcode)
|
273
282
|
return f"EXITED with code {exitcode}"
|
autosh/plugins/clock.py
CHANGED
@@ -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()
|
autosh/plugins/code.py
CHANGED
@@ -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
|
"""
|
autosh/plugins/search.py
CHANGED
@@ -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[
|
autosh/plugins/web.py
CHANGED
@@ -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"],
|
autosh/session.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
from pathlib import Path
|
2
|
+
import socket
|
2
3
|
import sys
|
3
4
|
from agentia import (
|
4
5
|
Agent,
|
@@ -14,10 +15,10 @@ from neongrid.loading import Loading
|
|
14
15
|
|
15
16
|
from autosh.config import CLI_OPTIONS, CONFIG
|
16
17
|
import neongrid as ng
|
17
|
-
from .plugins import create_plugins
|
18
|
+
from .plugins import Banner, create_plugins
|
18
19
|
import rich
|
19
20
|
import platform
|
20
|
-
|
21
|
+
import os
|
21
22
|
|
22
23
|
|
23
24
|
INSTRUCTIONS = f"""
|
@@ -102,6 +103,46 @@ class Session:
|
|
102
103
|
role="user",
|
103
104
|
)
|
104
105
|
|
106
|
+
async def __process_event(self, e: Event, first: bool, repl: bool):
|
107
|
+
prefix_newline = repl or not first
|
108
|
+
if isinstance(e, UserConsentEvent):
|
109
|
+
if CLI_OPTIONS.yes:
|
110
|
+
e.response = True
|
111
|
+
return False
|
112
|
+
if prefix_newline:
|
113
|
+
print()
|
114
|
+
e.response = ng.confirm(e.message)
|
115
|
+
return True
|
116
|
+
if isinstance(e, ToolCallEvent) and e.result is None:
|
117
|
+
if (banner := (e.metadata or {}).get("banner")) and isinstance(
|
118
|
+
banner, Banner
|
119
|
+
):
|
120
|
+
return banner.render(e.arguments, prefix_newline=prefix_newline)
|
121
|
+
return False
|
122
|
+
|
123
|
+
async def __process_run(
|
124
|
+
self, run: Run[Event | MessageStream], loading: Loading | None, repl: bool
|
125
|
+
):
|
126
|
+
first = True
|
127
|
+
async for e in run:
|
128
|
+
if loading:
|
129
|
+
await loading.finish()
|
130
|
+
|
131
|
+
if isinstance(e, Event):
|
132
|
+
if await self.__process_event(e, first=first, repl=repl):
|
133
|
+
first = False
|
134
|
+
else:
|
135
|
+
if repl or not first:
|
136
|
+
print()
|
137
|
+
await self.__render_streamed_markdown(e)
|
138
|
+
first = False
|
139
|
+
|
140
|
+
if loading:
|
141
|
+
loading = self.__create_loading_indicator()
|
142
|
+
|
143
|
+
if loading:
|
144
|
+
await loading.finish()
|
145
|
+
|
105
146
|
async def exec_prompt(self, prompt: str):
|
106
147
|
# Clean up the prompt
|
107
148
|
if prompt is not None:
|
@@ -142,6 +183,8 @@ class Session:
|
|
142
183
|
)
|
143
184
|
run = self.agent.run(prompt, stream=True, events=True)
|
144
185
|
await self.__process_run(run, loading, False)
|
186
|
+
if CLI_OPTIONS.start_repl_after_prompt:
|
187
|
+
await self.run_repl(handover=True)
|
145
188
|
|
146
189
|
async def exec_from_stdin(self):
|
147
190
|
if sys.stdin.isatty():
|
@@ -158,51 +201,19 @@ class Session:
|
|
158
201
|
prompt = f.read()
|
159
202
|
await self.exec_prompt(prompt)
|
160
203
|
|
161
|
-
async def
|
162
|
-
if
|
163
|
-
|
164
|
-
|
165
|
-
if banner := (e.metadata or {}).get("banner"):
|
166
|
-
banner(e.arguments)
|
167
|
-
|
168
|
-
async def __confirm(self, message: str) -> bool:
|
169
|
-
if CLI_OPTIONS.yes:
|
170
|
-
return True
|
171
|
-
result = Confirm.ask(
|
172
|
-
f"\n[magenta]{message}[/magenta]", default=True, case_sensitive=False
|
173
|
-
)
|
174
|
-
return result
|
175
|
-
|
176
|
-
async def __process_run(
|
177
|
-
self, run: Run[Event | MessageStream], loading: Loading | None, repl: bool
|
178
|
-
):
|
179
|
-
async for e in run:
|
180
|
-
if loading:
|
181
|
-
await loading.finish()
|
182
|
-
|
183
|
-
if isinstance(e, Event):
|
184
|
-
await self.__process_event(e)
|
185
|
-
else:
|
186
|
-
if repl or not CLI_OPTIONS.quiet:
|
187
|
-
print()
|
188
|
-
await self.__render_streamed_markdown(e)
|
189
|
-
|
190
|
-
if loading:
|
191
|
-
loading = self.__create_loading_indicator()
|
192
|
-
|
193
|
-
if loading:
|
194
|
-
await loading.finish()
|
195
|
-
|
196
|
-
async def run_repl(self):
|
197
|
-
first = True
|
204
|
+
async def run_repl(self, handover: bool = False):
|
205
|
+
if not handover and CONFIG.repl_banner:
|
206
|
+
rich.print(CONFIG.repl_banner)
|
207
|
+
first = not handover
|
198
208
|
while True:
|
199
209
|
try:
|
200
210
|
if not first:
|
201
211
|
print()
|
202
212
|
first = False
|
203
|
-
|
204
|
-
|
205
|
-
|
213
|
+
input_prompt = self.__get_input_prompt()
|
214
|
+
rich.print(input_prompt, end="", flush=True)
|
215
|
+
prompt = await ng.input("", sync=False, persist="/tmp/autosh-history")
|
216
|
+
prompt = prompt.strip()
|
206
217
|
if prompt in ["exit", "quit"]:
|
207
218
|
break
|
208
219
|
if len(prompt) == 0:
|
@@ -213,6 +224,34 @@ class Session:
|
|
213
224
|
except KeyboardInterrupt:
|
214
225
|
break
|
215
226
|
|
227
|
+
def __get_input_prompt(self):
|
228
|
+
cwd = Path.cwd()
|
229
|
+
relative_to_home = False
|
230
|
+
if cwd.is_relative_to(Path.home()):
|
231
|
+
cwd = cwd.relative_to(Path.home())
|
232
|
+
relative_to_home = True
|
233
|
+
# short cwd
|
234
|
+
short_cwd = "/" if not relative_to_home else "~/"
|
235
|
+
parts = []
|
236
|
+
for i, p in enumerate(cwd.parts):
|
237
|
+
if i == 0 and p == "/":
|
238
|
+
continue
|
239
|
+
if i != len(cwd.parts) - 1:
|
240
|
+
parts.append(p[0])
|
241
|
+
else:
|
242
|
+
parts.append(p)
|
243
|
+
short_cwd += "/".join(parts)
|
244
|
+
cwd = str(cwd) if not relative_to_home else "~/" + str(cwd)
|
245
|
+
host = socket.gethostname()
|
246
|
+
user = os.getlogin()
|
247
|
+
prompt = CONFIG.repl_prompt.format(
|
248
|
+
cwd=cwd,
|
249
|
+
short_cwd=short_cwd,
|
250
|
+
host=host,
|
251
|
+
user=user,
|
252
|
+
)
|
253
|
+
return prompt
|
254
|
+
|
216
255
|
def __create_loading_indicator(self):
|
217
256
|
return ng.loading.kana()
|
218
257
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: autosh
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.8
|
4
4
|
Summary: The AI-powered, noob-friendly interactive shell
|
5
5
|
Author-email: Wenyu Zhao <wenyuzhaox@gmail.com>
|
6
6
|
License-Expression: MIT
|
@@ -10,7 +10,7 @@ Requires-Python: >=3.12
|
|
10
10
|
Requires-Dist: agentia>=0.0.8
|
11
11
|
Requires-Dist: asyncio>=3.4.3
|
12
12
|
Requires-Dist: markdownify>=1.1.0
|
13
|
-
Requires-Dist: neongrid>=0.0.
|
13
|
+
Requires-Dist: neongrid>=0.0.2
|
14
14
|
Requires-Dist: prompt-toolkit>=3.0.51
|
15
15
|
Requires-Dist: pydantic>=2.11.3
|
16
16
|
Requires-Dist: python-dotenv>=1.1.0
|
@@ -37,7 +37,7 @@ As an interactive shell: `ash` (alternatively, `autosh`)
|
|
37
37
|
Execute a single prompt: `ash "list current directory"`
|
38
38
|
|
39
39
|
Process piped data:
|
40
|
-
* `cat README.md | ash -y "
|
40
|
+
* `cat README.md | ash -y "summarize"`
|
41
41
|
* `cat in.csv | ash -y -q "double the first numeric column" > out.csv`
|
42
42
|
|
43
43
|
## Scripting
|
@@ -56,15 +56,15 @@ First, please display a welcome message:)
|
|
56
56
|
Write "Hello, world" to _test.log
|
57
57
|
```
|
58
58
|
|
59
|
-
* Run the script: `ash simple.a.md` or `chmod +x simple.a.md && simple.a.md`
|
60
|
-
* Auto
|
59
|
+
* Run the script: `ash simple.a.md` or `chmod +x simple.a.md && ./simple.a.md`
|
60
|
+
* Auto-generate help messages:
|
61
61
|
|
62
62
|
```console
|
63
63
|
$ ash simple.a.md -h
|
64
64
|
|
65
65
|
Usage: simple.a.md [OPTIONS]
|
66
66
|
|
67
|
-
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.
|
68
68
|
|
69
69
|
Options:
|
70
70
|
|
@@ -73,9 +73,9 @@ Write "Hello, world" to _test.log
|
|
73
73
|
|
74
74
|
## Plugins
|
75
75
|
|
76
|
-
`autosh`
|
76
|
+
`autosh` comes with several plugins to expand its capabilities:
|
77
77
|
|
78
|
-
* `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"`
|
79
79
|
|
80
80
|
# TODO
|
81
81
|
|
@@ -83,4 +83,4 @@ Write "Hello, world" to _test.log
|
|
83
83
|
- [ ] RAG for non-text files
|
84
84
|
- [ ] Plugin system
|
85
85
|
- [ ] MCP support
|
86
|
-
- [
|
86
|
+
- [x] Improved input widget with history and auto-completion
|
@@ -0,0 +1,17 @@
|
|
1
|
+
autosh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
autosh/config-template.toml,sha256=iLdCBHIK0czWgNHtwAgvuhV670aiNc-IOmaPHP12i0Y,269
|
3
|
+
autosh/config.py,sha256=TrEcQEEfWdruQntEJwxDUoHpBBkVpK_n2FJ-3IjDYHU,3060
|
4
|
+
autosh/main.py,sha256=Fvaxg-vkpEITZRxLxOXs_sE4_tZ8P_F0PKxQzZF455o,5782
|
5
|
+
autosh/session.py,sha256=1uPLDa37tIgFjclv2lqj5VnOkoy77tqyF7nTdK261B8,9391
|
6
|
+
autosh/plugins/__init__.py,sha256=I_lDaPxoPTiAVegpCGV3LpdBLQl4dbKmWPBZjQrmGIs,3260
|
7
|
+
autosh/plugins/calc.py,sha256=n37BxwnBZW8Fez8VeN2CRizkZlUgbVGCuip_8lGxzBs,606
|
8
|
+
autosh/plugins/cli.py,sha256=5u96BySehoejyqTOAaJIqUx4bmymhk5xmjVVCrz-XJ0,9375
|
9
|
+
autosh/plugins/clock.py,sha256=4a_zpEggzJilgIA1DPP7ZdGA8cbYYH6hOL3MotkzpUk,556
|
10
|
+
autosh/plugins/code.py,sha256=jMw4YWRkmvSKZ24DiVtXjxhfbuV7qs4x6o0IJLYFszE,2423
|
11
|
+
autosh/plugins/search.py,sha256=_ey7myAbTILi3fbWfl6gGBbkPi8txP3dIKVL27o4ZVw,2798
|
12
|
+
autosh/plugins/web.py,sha256=7DT7adgN6FPlrmGvnHynilh_WZevjWZaHiNa0egNXHs,2618
|
13
|
+
autosh-0.0.8.dist-info/METADATA,sha256=ibtORCq9rOBYc1HlS8PUi34X29dtSlOpRRIHCdamW2Y,2165
|
14
|
+
autosh-0.0.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
15
|
+
autosh-0.0.8.dist-info/entry_points.txt,sha256=BV7bzUnxG6Z5InEkrfajGCxjooYORC5tZDDZctOPenQ,67
|
16
|
+
autosh-0.0.8.dist-info/licenses/LICENSE,sha256=BnLDJsIJe-Dm18unR9DOoSv7QOfAz6LeIQc1yHAjxp0,1066
|
17
|
+
autosh-0.0.8.dist-info/RECORD,,
|
autosh-0.0.6.dist-info/RECORD
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
autosh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
autosh/config-template.toml,sha256=iLdCBHIK0czWgNHtwAgvuhV670aiNc-IOmaPHP12i0Y,269
|
3
|
-
autosh/config.py,sha256=7DXUSsqHm8dPsXln-NTu8D0_Uwcs2h4sT8pFtQM8tso,2654
|
4
|
-
autosh/main.py,sha256=bgWkYpU9cmyU2AyWt4o8Mcj6LH_UGYSYrFx4Qkl8vqk,5117
|
5
|
-
autosh/session.py,sha256=AOhDTuTg9MlqW-VDKmHsiqqEg7PkDMUj0mknubE97cQ,7962
|
6
|
-
autosh/plugins/__init__.py,sha256=LzaRWYpXv-e2YPr3VP1Ggd1rjNlQDjGRMPVTK7Hdfnk,2940
|
7
|
-
autosh/plugins/calc.py,sha256=x_fZW3kkStlAAQEhejcD2_MQnpqCd5ghU72CNSjqIsg,672
|
8
|
-
autosh/plugins/cli.py,sha256=P8orcjsTfD_k7oEmaoW4xrrIpzg_XXKvV8ok3br6B44,9299
|
9
|
-
autosh/plugins/clock.py,sha256=gIrYBz3GmnAJTujzd3B8ewuThXpF-OvWYBugcZMYGTQ,570
|
10
|
-
autosh/plugins/code.py,sha256=HK7cd7gNit2yZAukaCnTGXb6BAh31cPh0EHCx6msOWc,2457
|
11
|
-
autosh/plugins/search.py,sha256=aWbIDJ7E5m-Xrw2A1DB6w53oGiFe1Uc3EuZikIH5eTs,2984
|
12
|
-
autosh/plugins/web.py,sha256=LxE7ed7IS9B4spWCm-3aBMLfC9ghcNnmJzMiD6ayLdc,2648
|
13
|
-
autosh-0.0.6.dist-info/METADATA,sha256=3Bkxyj284hbhWISqxLtQQ3jbjSxX6S6cGYhmlvGuboA,2154
|
14
|
-
autosh-0.0.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
15
|
-
autosh-0.0.6.dist-info/entry_points.txt,sha256=BV7bzUnxG6Z5InEkrfajGCxjooYORC5tZDDZctOPenQ,67
|
16
|
-
autosh-0.0.6.dist-info/licenses/LICENSE,sha256=BnLDJsIJe-Dm18unR9DOoSv7QOfAz6LeIQc1yHAjxp0,1066
|
17
|
-
autosh-0.0.6.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|