autosh 0.0.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autosh
3
- Version: 0.0.6
3
+ Version: 0.0.7
4
4
  Summary: The AI-powered, noob-friendly interactive shell
5
5
  Author-email: Wenyu Zhao <wenyuzhaox@gmail.com>
6
6
  License-Expression: MIT
@@ -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 "summarise"`
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 generate help messages:
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 _x.log.
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` is equipped with several plugins to expand its potential:
76
+ `autosh` comes with several plugins to expand its capabilities:
77
77
 
78
- * `ash "Create a directory "my-news", list the latest news, for each news, put the summary in a separate markdown file in this 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
- - [ ] A better input widget with history and auto completion
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 "summarise"`
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 generate help messages:
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 _x.log.
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` is equipped with several plugins to expand its potential:
54
+ `autosh` comes with several plugins to expand its capabilities:
55
55
 
56
- * `ash "Create a directory "my-news", list the latest news, for each news, put the summary in a separate markdown file in this 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
- - [ ] A better input widget with history and auto completion
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
- if piped_stdin and not CLI_OPTIONS.yes:
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 simple_banner
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, code_preview_banner, simple_banner
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": simple_banner("CWD", dim=lambda a: a.get("path", ""))})
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": simple_banner("GET ARGV")})
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": simple_banner("GET ENV", dim=lambda a: a.get("key", ""))})
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": simple_banner("GET ALL ENVS")})
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": simple_banner(
106
- tag=lambda a: "SET ENV" if a.get("value") else "DEL ENV",
107
- text=lambda a: a.get("key", ""),
108
- dim=lambda a: a.get("value", "") or "",
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": simple_banner("READ", dim=lambda a: a.get("path", ""))})
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": simple_banner(
149
- tag=lambda a: "WRITE" if not a.get("append") else "APPEND",
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": code_preview_banner(
215
+ "banner": Banner(
214
216
  title="Run Command",
215
- short=lambda a: f"[magenta][bold]➜[/bold] [italic]{a.get("command", "")}[/italic][/magenta]",
216
- content=lambda a: f"[magenta][bold]➜[/bold] [italic]{a.get("command", "")}[/italic][/magenta]\n\n[dim]{a.get("explanation", "")}[/dim]",
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": simple_banner("EXIT")})
268
- def exit(self, exitcode: Annotated[int, "The exit code of this shell session"] = 0):
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}"
@@ -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 simple_banner
5
+ from autosh.plugins import Banner
6
6
 
7
7
 
8
8
  class ClockPlugin(Plugin):
9
- @tool(metadata={"banner": simple_banner("GET TIME")})
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 code_preview_banner, code_result_panel
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": code_preview_banner(
22
- title="Run Python",
23
- short="[bold]RUN[/bold] [italic]Python Code[/italic]",
24
- content=lambda a: code_with_explanation(
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, "Explain what this code does, and what are you going to use it for."
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 simple_banner
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 simple_banner
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": simple_banner("BROWSE", dim=lambda a: a.get("url", ""))})
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,4 +1,6 @@
1
+ from io import StringIO
1
2
  from pathlib import Path
3
+ import socket
2
4
  import sys
3
5
  from agentia import (
4
6
  Agent,
@@ -14,10 +16,11 @@ from neongrid.loading import Loading
14
16
 
15
17
  from autosh.config import CLI_OPTIONS, CONFIG
16
18
  import neongrid as ng
17
- from .plugins import create_plugins
19
+ from .plugins import Banner, create_plugins
18
20
  import rich
19
21
  import platform
20
22
  from rich.prompt import Confirm
23
+ import os
21
24
 
22
25
 
23
26
  INSTRUCTIONS = f"""
@@ -158,34 +161,39 @@ class Session:
158
161
  prompt = f.read()
159
162
  await self.exec_prompt(prompt)
160
163
 
161
- 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
162
166
  if isinstance(e, UserConsentEvent):
163
- e.response = await self.__confirm(e.message)
164
- if isinstance(e, ToolCallEvent) and e.result is None:
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:
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)
170
173
  return True
171
- result = Confirm.ask(
172
- f"\n[magenta]{message}[/magenta]", default=True, case_sensitive=False
173
- )
174
- return result
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
175
180
 
176
181
  async def __process_run(
177
182
  self, run: Run[Event | MessageStream], loading: Loading | None, repl: bool
178
183
  ):
184
+ first = True
179
185
  async for e in run:
180
186
  if loading:
181
187
  await loading.finish()
182
188
 
183
189
  if isinstance(e, Event):
184
- await self.__process_event(e)
190
+ if await self.__process_event(e, first=first, repl=repl):
191
+ first = False
185
192
  else:
186
- if repl or not CLI_OPTIONS.quiet:
193
+ if repl or not first:
187
194
  print()
188
195
  await self.__render_streamed_markdown(e)
196
+ first = False
189
197
 
190
198
  if loading:
191
199
  loading = self.__create_loading_indicator()
@@ -193,16 +201,47 @@ class Session:
193
201
  if loading:
194
202
  await loading.finish()
195
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
231
+
196
232
  async def run_repl(self):
233
+ if CONFIG.repl_banner:
234
+ rich.print(CONFIG.repl_banner)
197
235
  first = True
198
236
  while True:
199
237
  try:
200
238
  if not first:
201
239
  print()
202
240
  first = False
203
- prompt = (
204
- await ng.input("> ", sync=False, persist="/tmp/autosh-history")
205
- ).strip()
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()
206
245
  if prompt in ["exit", "quit"]:
207
246
  break
208
247
  if len(prompt) == 0:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "autosh"
3
- version = "0.0.6"
3
+ version = "0.0.7"
4
4
  description = "The AI-powered, noob-friendly interactive shell"
5
5
  authors = [{ name = "Wenyu Zhao", email = "wenyuzhaox@gmail.com" }]
6
6
  requires-python = ">=3.12"
@@ -1,101 +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
-
43
-
44
- def code_preview_banner(
45
- title: str | Callable[[Any], str],
46
- short: str | Callable[[Any], str],
47
- content: Callable[[Any], RenderableType],
48
- ):
49
- return lambda x: __print_code_preview_banner(
50
- title=title if isinstance(title, str) else title(x),
51
- content=content(x),
52
- short=short if isinstance(short, str) else short(x),
53
- )
54
-
55
-
56
- def code_result_panel(
57
- title: str,
58
- out: str | None = None,
59
- err: str | None = None,
60
- ):
61
- if CLI_OPTIONS.quiet:
62
- return
63
- print()
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
-
76
-
77
- from . import calc
78
- from . import clock
79
- from . import code
80
- from . import search
81
- from . import web
82
- from . import cli
83
-
84
-
85
- def create_plugins():
86
- """Get all plugins in the autosh.plugins module."""
87
- cfgs = CONFIG.plugins
88
- plugins = []
89
- if cfgs.calc is not None:
90
- plugins.append(calc.CalculatorPlugin(cfgs.calc.model_dump()))
91
- if cfgs.cli is not None:
92
- plugins.append(cli.CLIPlugin(cfgs.cli.model_dump()))
93
- if cfgs.clock is not None:
94
- plugins.append(clock.ClockPlugin(cfgs.clock.model_dump()))
95
- if cfgs.code is not None:
96
- plugins.append(code.CodePlugin(cfgs.code.model_dump()))
97
- if cfgs.search is not None:
98
- plugins.append(search.SearchPlugin(cfgs.search.model_dump()))
99
- if cfgs.web is not None:
100
- plugins.append(web.WebPlugin(cfgs.web.model_dump()))
101
- return plugins
File without changes
File without changes
File without changes