autosh 0.0.6__tar.gz → 0.0.8__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.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.1
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 "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,
@@ -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"""
@@ -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
- 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)
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:
@@ -0,0 +1,109 @@
1
+ from dataclasses import dataclass
2
+ import sys
3
+ from typing import Any, Callable
4
+ import rich
5
+ from rich.panel import Panel
6
+ from rich.console import RenderableType
7
+ from autosh.config import CLI_OPTIONS, CONFIG
8
+
9
+
10
+ @dataclass
11
+ class Banner:
12
+ title: str | Callable[[Any], str]
13
+
14
+ text: str | Callable[[Any], str] | None = None
15
+
16
+ text_key: str | None = None
17
+
18
+ code: Callable[[Any], RenderableType] | None = None
19
+ """
20
+ Turn the banner into a code block
21
+ """
22
+
23
+ user_consent: bool = False
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
31
+
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
62
+
63
+
64
+ def code_result_panel(
65
+ title: str,
66
+ out: str | None = None,
67
+ err: str | None = None,
68
+ ):
69
+ if CLI_OPTIONS.quiet:
70
+ return
71
+ print()
72
+ if isinstance(out, str):
73
+ out = out.strip()
74
+ if isinstance(err, str):
75
+ err = err.strip()
76
+ if not out and not err:
77
+ rich.print(title)
78
+ else:
79
+ text = out if out else ""
80
+ text += (("\n---\n" if out else "") + err) if err else ""
81
+ panel = Panel.fit(text, title=title, title_align="left", style="dim")
82
+ rich.print(panel)
83
+
84
+
85
+ from . import calc
86
+ from . import clock
87
+ from . import code
88
+ from . import search
89
+ from . import web
90
+ from . import cli
91
+
92
+
93
+ def create_plugins():
94
+ """Get all plugins in the autosh.plugins module."""
95
+ cfgs = CONFIG.plugins
96
+ plugins = []
97
+ if cfgs.calc is not None:
98
+ plugins.append(calc.CalculatorPlugin(cfgs.calc.model_dump()))
99
+ if cfgs.cli is not None:
100
+ plugins.append(cli.CLIPlugin(cfgs.cli.model_dump()))
101
+ if cfgs.clock is not None:
102
+ plugins.append(clock.ClockPlugin(cfgs.clock.model_dump()))
103
+ if cfgs.code is not None:
104
+ plugins.append(code.CodePlugin(cfgs.code.model_dump()))
105
+ if cfgs.search is not None:
106
+ plugins.append(search.SearchPlugin(cfgs.search.model_dump()))
107
+ if cfgs.web is not None:
108
+ plugins.append(web.WebPlugin(cfgs.web.model_dump()))
109
+ 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,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
- from rich.prompt import Confirm
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 __process_event(self, e: Event):
162
- 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:
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
- prompt = (
204
- await ng.input("> ", sync=False, persist="/tmp/autosh-history")
205
- ).strip()
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
  [project]
2
2
  name = "autosh"
3
- version = "0.0.6"
3
+ version = "0.0.8"
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"
@@ -29,7 +29,7 @@ dependencies = [
29
29
  "tavily-python>=0.5.4",
30
30
  "typer>=0.12.5",
31
31
  "tzlocal>=5.3.1",
32
- "neongrid>=0.0.1",
32
+ "neongrid>=0.0.2",
33
33
  "agentia>=0.0.8",
34
34
  ]
35
35
 
@@ -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