autosh 0.0.5__py3-none-any.whl → 0.0.7__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 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,
autosh/main.py CHANGED
@@ -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
 
@@ -1,3 +1,5 @@
1
+ from dataclasses import dataclass
2
+ import sys
1
3
  from typing import Any, Callable
2
4
  import rich
3
5
  from rich.prompt import Confirm
@@ -6,52 +8,108 @@ from rich.console import RenderableType
6
8
  from autosh.config import CLI_OPTIONS, CONFIG
7
9
 
8
10
 
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
- )
11
+ @dataclass
12
+ class Banner:
13
+ title: str | Callable[[Any], str]
30
14
 
15
+ text: str | Callable[[Any], str] | None = None
31
16
 
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()
17
+ text_key: str | None = None
43
18
 
19
+ code: Callable[[Any], RenderableType] | None = None
20
+ """
21
+ Turn the banner into a code block
22
+ """
44
23
 
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
- )
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
+ # )
55
113
 
56
114
 
57
115
  def code_result_panel(
@@ -61,6 +119,7 @@ def code_result_panel(
61
119
  ):
62
120
  if CLI_OPTIONS.quiet:
63
121
  return
122
+ print()
64
123
  if isinstance(out, str):
65
124
  out = out.strip()
66
125
  if isinstance(err, str):
@@ -72,8 +131,6 @@ def code_result_panel(
72
131
  text += (("\n---\n" if out else "") + err) if err else ""
73
132
  panel = Panel.fit(text, title=title, title_align="left", style="dim")
74
133
  rich.print(panel)
75
- if not CLI_OPTIONS.quiet:
76
- rich.print()
77
134
 
78
135
 
79
136
  from . import calc
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 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[
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, 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.
@@ -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": simple_banner("GET ARGV")})
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": simple_banner("GET ENV", dim=lambda a: a.get("key", ""))})
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": simple_banner("GET ALL ENVS")})
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": simple_banner(
105
- tag=lambda a: "SET ENV" if a.get("value") else "DEL ENV",
106
- text=lambda a: a.get("key", ""),
107
- 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
+ ),
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": simple_banner("READ", dim=lambda a: a.get("path", ""))})
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": simple_banner(
148
- tag=lambda a: "WRITE" if not a.get("append") else "APPEND",
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": code_preview_banner(
215
+ "banner": Banner(
213
216
  title="Run Command",
214
- short=lambda a: f"[magenta][bold]➜[/bold] [italic]{a.get("command", "")}[/italic][/magenta]",
215
- 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,
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": simple_banner("EXIT")})
267
- 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
+ ):
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}"
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 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()
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 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
  """
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 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[
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 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"],
autosh/session.py CHANGED
@@ -1,5 +1,6 @@
1
- import asyncio
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
- ChatCompletion,
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
- completion = agent.chat_completion(prompt, stream=True)
91
- async for stream in completion:
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
- completion = self.agent.chat_completion(prompt, stream=True, events=True)
145
- await self.__process_completion(completion, loading)
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
- e.response = await self.__confirm(e.message)
165
- if isinstance(e, ToolCallEvent) and e.result is None:
166
- if banner := (e.metadata or {}).get("banner"):
167
- banner(e.arguments)
168
-
169
- async def __confirm(self, message: str) -> bool:
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 = Confirm.ask(
173
- f"[magenta]{message}[/magenta]", default=True, case_sensitive=False
174
- )
175
- if not CLI_OPTIONS.quiet:
176
- rich.print()
177
- 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
178
180
 
179
- async def __process_completion(
180
- self, completion: ChatCompletion[Event | MessageStream], loading: Loading
181
+ async def __process_run(
182
+ self, run: Run[Event | MessageStream], loading: Loading | None, repl: bool
181
183
  ):
182
- async for stream in completion:
183
- await loading.finish()
184
+ first = True
185
+ async for e in run:
186
+ if loading:
187
+ await loading.finish()
184
188
 
185
- if isinstance(stream, Event):
186
- await self.__process_event(stream)
189
+ if isinstance(e, Event):
190
+ if await self.__process_event(e, first=first, repl=repl):
191
+ first = False
187
192
  else:
188
- print()
189
- await self.__render_streamed_markdown(stream)
190
- print()
193
+ if repl or not first:
194
+ print()
195
+ await self.__render_streamed_markdown(e)
196
+ first = False
191
197
 
192
- loading = self.__create_loading_indicator()
198
+ if loading:
199
+ loading = self.__create_loading_indicator()
193
200
 
194
- await loading.finish()
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
- prompt = (
200
- await ng.input("> ", sync=False, persist="/tmp/autosh-history")
201
- ).strip()
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
- completion = self.agent.chat_completion(
208
- prompt, stream=True, events=True
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,10 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autosh
3
- Version: 0.0.5
4
- Summary: Add your description here
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
- Requires-Python: >=3.13
7
- Requires-Dist: agentia>=0.0.7
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 "summarise"`
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 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:
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 _x.log.
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` is equipped with several plugins to expand its potential:
76
+ `autosh` comes with several plugins to expand its capabilities:
74
77
 
75
- * `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"`
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
- - [ ] A better input widget with history and auto completion
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=TvKXwOjdVXFOVY91M8QK7j8XFBUvWk_drsaw1v5ZTIw,3018
4
+ autosh/main.py,sha256=vCrnpTXTXD6evjuePqUrNd4q-_qRDk0g0VqW_Gi-prQ,5242
5
+ autosh/session.py,sha256=nVCFDEQQ61Doh7e4daMdorMsCL8LxzjWrDtwerPy1Ks,9303
6
+ autosh/plugins/__init__.py,sha256=T6HAgYUIz5XpwnyqciSmjaiEKEWmeKOODcwxntNXqMM,4797
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.7.dist-info/METADATA,sha256=e9SUTlYaQEDoSvv3KuwlvOPqSmMuUmBge2fojjX5iWc,2165
14
+ autosh-0.0.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
+ autosh-0.0.7.dist-info/entry_points.txt,sha256=BV7bzUnxG6Z5InEkrfajGCxjooYORC5tZDDZctOPenQ,67
16
+ autosh-0.0.7.dist-info/licenses/LICENSE,sha256=BnLDJsIJe-Dm18unR9DOoSv7QOfAz6LeIQc1yHAjxp0,1066
17
+ autosh-0.0.7.dist-info/RECORD,,
@@ -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=fOJySGU9abjFReNOyvQf-s9rFQdGy8Pv01GgplqrRuw,7943
6
- autosh/plugins/__init__.py,sha256=oUtO7M1AuZKshbaS-kO0fbV4CYal0uOvg-GpYVFE16I,2996
7
- autosh/plugins/calc.py,sha256=x_fZW3kkStlAAQEhejcD2_MQnpqCd5ghU72CNSjqIsg,672
8
- autosh/plugins/cli.py,sha256=G4WqTWeCi7OxdB36lAUiR1OBdtSvxmKRcQGGLdMcxms,9230
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.5.dist-info/METADATA,sha256=gd36RdBvQc9ZrqJYuDjvLGjDM8UuNdh9xKffeS96jDQ,1966
14
- autosh-0.0.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
- autosh-0.0.5.dist-info/entry_points.txt,sha256=BV7bzUnxG6Z5InEkrfajGCxjooYORC5tZDDZctOPenQ,67
16
- autosh-0.0.5.dist-info/licenses/LICENSE,sha256=BnLDJsIJe-Dm18unR9DOoSv7QOfAz6LeIQc1yHAjxp0,1066
17
- autosh-0.0.5.dist-info/RECORD,,
File without changes