autosh 0.0.2__py3-none-any.whl → 0.0.4__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.
@@ -0,0 +1,13 @@
1
+ # autosh configuration file
2
+ [autosh]
3
+ api_key = "sk-or-v1-..."
4
+ # model = "openai/gpt-4.1"
5
+ # think_model = "openai/o4-mini-high"
6
+
7
+ [plugins]
8
+ calc = {}
9
+ cli = {}
10
+ clock = {}
11
+ code = {}
12
+ # search = { tavily_api_key = "tvly-dev-..." }
13
+ # web = { tavily_api_key = "tvly-dev-..." }
autosh/config.py CHANGED
@@ -43,6 +43,11 @@ class Config(BaseModel):
43
43
 
44
44
  @staticmethod
45
45
  def load() -> "Config":
46
+ if not USER_CONFIG_PATH.is_file():
47
+ # Copy config.template.toml to USER_CONFIG_PATH
48
+ USER_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
49
+ template = Path(__file__).parent / "config.template.toml"
50
+ USER_CONFIG_PATH.write_text(template.read_text())
46
51
  if USER_CONFIG_PATH.is_file():
47
52
  try:
48
53
  doc = tomllib.loads(USER_CONFIG_PATH.read_text())
autosh/main.py CHANGED
@@ -1,14 +1,16 @@
1
1
  import os
2
+
3
+ os.environ["AGENTIA_DISABLE_PLUGINS"] = "1"
4
+
2
5
  from pathlib import Path
3
6
  import rich
4
7
  import typer
5
8
  import asyncio
6
- import dotenv
7
9
  from rich.columns import Columns
8
10
  from rich.panel import Panel
9
11
  import argparse
10
12
 
11
- from autosh.config import CLI_OPTIONS, CONFIG
13
+ from autosh.config import CLI_OPTIONS, CONFIG, USER_CONFIG_PATH
12
14
  from .session import Session
13
15
  import sys
14
16
 
@@ -26,6 +28,8 @@ app = typer.Typer(
26
28
  async def start_session(prompt: str | None, args: list[str]):
27
29
  CLI_OPTIONS.args = args
28
30
  session = Session()
31
+ os.environ["OPENROUTER_HAS_REASONING"] = "false"
32
+ os.environ["OPENROUTER_INCLUDE_REASONING"] = "false"
29
33
  await session.init()
30
34
  piped_stdin = not sys.stdin.isatty()
31
35
  if piped_stdin and not CLI_OPTIONS.yes:
@@ -149,14 +153,13 @@ def main():
149
153
  # dotenv.load_dotenv()
150
154
  prompt, args = parse_args()
151
155
 
156
+ if key := os.getenv("OPENROUTER_API_KEY"):
157
+ CONFIG.api_key = key
152
158
  if CONFIG.api_key is None:
153
- if key := os.getenv("OPENROUTER_API_KEY"):
154
- CONFIG.api_key = key
155
- else:
156
- rich.print(
157
- "[bold red]Error:[/bold red] [red]No API key found. Please set the OPENROUTER_API_KEY environment variable or add it to your config file.[/red]"
158
- )
159
- sys.exit(1)
159
+ rich.print(
160
+ f"[bold red]Error:[/bold red] [red]OpenRouter API key not found.\nPlease set the OPENROUTER_API_KEY environment variable or add it to your config file: {USER_CONFIG_PATH}.[/red]"
161
+ )
162
+ sys.exit(1)
160
163
  try:
161
164
  asyncio.run(start_session(prompt, args))
162
165
  except (KeyboardInterrupt, EOFError):
@@ -1,3 +1,4 @@
1
+ from typing import Any, Callable
1
2
  import rich
2
3
  from rich.prompt import Confirm
3
4
  from rich.panel import Panel
@@ -5,40 +6,55 @@ from rich.console import RenderableType
5
6
  from autosh.config import CLI_OPTIONS, CONFIG
6
7
 
7
8
 
8
- def banner(tag: str, text: str | None = None, dim: str | None = None):
9
+ def __print_simple_banner(tag: str, text: str | None = None, dim: str | None = None):
9
10
  if CLI_OPTIONS.quiet:
10
11
  return
11
- s = f"[bold magenta]{tag}[/bold magenta]"
12
+ s = f"\n[bold on magenta] {tag} [/bold on magenta]"
12
13
  if text:
13
14
  s += f" [italic magenta]{text}[/italic magenta]"
14
15
  if dim:
15
16
  s += f" [italic dim]{dim}[/italic dim]"
16
- s += "\n"
17
17
  rich.print(s)
18
18
 
19
19
 
20
- def confirm(message: str):
21
- if CLI_OPTIONS.yes:
22
- return True
23
- result = Confirm.ask(
24
- f"[magenta]{message}[/magenta]", default=True, case_sensitive=False
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,
25
29
  )
26
- if not CLI_OPTIONS.quiet:
27
- rich.print()
28
- return result
29
30
 
30
31
 
31
- def cmd_preview_panel(title: str, content: RenderableType, short: str | None = None):
32
- if CLI_OPTIONS.quiet and not CLI_OPTIONS.yes:
33
- if short:
34
- rich.print(f"[magenta]{short}[/magenta]\n")
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")
35
38
  return
36
39
  panel = Panel.fit(content, title=f"[magenta]{title}[/magenta]", title_align="left")
40
+ rich.print()
37
41
  rich.print(panel)
38
42
  rich.print()
39
43
 
40
44
 
41
- def cmd_result_panel(
45
+ def code_preview_banner(
46
+ title: str | Callable[[Any], str],
47
+ short: str | Callable[[Any], str],
48
+ content: Callable[[Any], RenderableType],
49
+ ):
50
+ return lambda x: __print_code_preview_banner(
51
+ title=title if isinstance(title, str) else title(x),
52
+ content=content(x),
53
+ short=short if isinstance(short, str) else short(x),
54
+ )
55
+
56
+
57
+ def code_result_panel(
42
58
  title: str,
43
59
  out: str | None = None,
44
60
  err: str | None = None,
autosh/plugins/calc.py CHANGED
@@ -1,12 +1,16 @@
1
1
  from agentia.plugins import tool, Plugin
2
2
  from typing import Annotated
3
- from . import banner
3
+ from . import simple_banner
4
4
 
5
5
 
6
6
  class CalculatorPlugin(Plugin):
7
7
  NAME = "calc"
8
8
 
9
- @tool
9
+ @tool(
10
+ metadata={
11
+ "banner": simple_banner("CALC", dim=lambda a: a.get("expression", ""))
12
+ }
13
+ )
10
14
  def evaluate(
11
15
  self,
12
16
  expression: Annotated[
@@ -16,7 +20,6 @@ class CalculatorPlugin(Plugin):
16
20
  """
17
21
  Execute a math expression and return the result. The expression must be an valid python expression that can be execuated by `eval()`.
18
22
  """
19
- banner("CALC", expression)
20
23
 
21
24
  result = eval(expression)
22
25
  return result
autosh/plugins/cli.py CHANGED
@@ -1,14 +1,14 @@
1
1
  import os
2
2
  import sys
3
3
  from typing import Annotated
4
- from agentia.plugins import Plugin, tool
4
+ from agentia import Plugin, tool, UserConsentEvent
5
5
  import rich
6
6
  import subprocess
7
7
  from enum import StrEnum
8
8
 
9
9
  from autosh.config import CLI_OPTIONS
10
10
 
11
- from . import banner, confirm, cmd_result_panel, cmd_preview_panel
11
+ from . import code_result_panel, code_preview_banner, simple_banner
12
12
 
13
13
 
14
14
  class Color(StrEnum):
@@ -59,22 +59,20 @@ 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
62
+ @tool(metadata={"banner": simple_banner("CWD", dim=lambda a: a.get("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.
66
66
  """
67
- banner("CWD", path)
68
67
  if not os.path.exists(path):
69
68
  raise FileNotFoundError(f"Path `{path}` does not exist.")
70
69
  os.chdir(path)
71
70
 
72
- @tool
71
+ @tool(metadata={"banner": simple_banner("GET ARGV")})
73
72
  def get_argv(self):
74
73
  """
75
74
  Get the command line arguments.
76
75
  """
77
- banner("GET ARGV")
78
76
  if not CLI_OPTIONS.script:
79
77
  return CLI_OPTIONS.args
80
78
  return {
@@ -82,28 +80,34 @@ class CLIPlugin(Plugin):
82
80
  "args": CLI_OPTIONS.args,
83
81
  }
84
82
 
85
- @tool
83
+ @tool(metadata={"banner": simple_banner("GET ENV", dim=lambda a: a.get("key", ""))})
86
84
  def get_env(self, key: Annotated[str, "The environment variable to get"]):
87
85
  """
88
86
  Get an environment variable.
89
87
  """
90
- banner("GET ENV", key)
91
88
  if key not in os.environ:
92
89
  raise KeyError(f"Environment variable `{key}` does not exist.")
93
90
  return os.environ[key]
94
91
 
95
- @tool
92
+ @tool(metadata={"banner": simple_banner("GET ALL ENVS")})
96
93
  def get_all_envs(self):
97
94
  """
98
95
  Get all environment variables.
99
96
  """
100
- banner("GET ALL ENVS")
101
97
  envs = {}
102
98
  for key, value in os.environ.items():
103
99
  envs[key] = value
104
100
  return {"envs": envs}
105
101
 
106
- @tool
102
+ @tool(
103
+ 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 "",
108
+ ),
109
+ }
110
+ )
107
111
  def update_env(
108
112
  self,
109
113
  key: Annotated[str, "The environment variable to set"],
@@ -116,15 +120,13 @@ class CLIPlugin(Plugin):
116
120
  Set or delete an environment variable.
117
121
  """
118
122
  if value is None:
119
- banner("DEL ENV", key)
120
123
  if key in os.environ:
121
124
  del os.environ[key]
122
125
  else:
123
- banner("SET ENV", key, value)
124
126
  os.environ[key] = value
125
127
  return f"DONE"
126
128
 
127
- @tool
129
+ @tool(metadata={"banner": simple_banner("READ", dim=lambda a: a.get("path", ""))})
128
130
  def read(
129
131
  self,
130
132
  path: Annotated[str, "The path to the file to read"],
@@ -132,7 +134,6 @@ class CLIPlugin(Plugin):
132
134
  """
133
135
  Read a file and print its content.
134
136
  """
135
- banner("READ", path)
136
137
  if not os.path.exists(path):
137
138
  raise FileNotFoundError(f"File `{path}` does not exist.")
138
139
  if not os.path.isfile(path):
@@ -141,7 +142,15 @@ class CLIPlugin(Plugin):
141
142
  content = f.read()
142
143
  return content
143
144
 
144
- @tool
145
+ @tool(
146
+ 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
+ ),
152
+ }
153
+ )
145
154
  def write(
146
155
  self,
147
156
  path: Annotated[str, "The path to the file to write"],
@@ -154,8 +163,6 @@ class CLIPlugin(Plugin):
154
163
  """
155
164
  Write or append text content to a file.
156
165
  """
157
- banner("WRITE" if not append else "APPEND", path, f"({len(content)} bytes)")
158
-
159
166
  if not create and not os.path.exists(path):
160
167
  raise FileNotFoundError(f"File `{path}` does not exist.")
161
168
  if not create and not os.path.isfile(path):
@@ -164,7 +171,7 @@ class CLIPlugin(Plugin):
164
171
  raise FileExistsError(
165
172
  f"No, you cannot overwrite the script file `{path}`. You're likely writing to it by mistake."
166
173
  )
167
- if not confirm("Write file?"):
174
+ if not (yield UserConsentEvent("Write file?")):
168
175
  return {"error": "The user declined the write operation."}
169
176
  flag = "a" if append else "w"
170
177
  if create:
@@ -200,7 +207,15 @@ class CLIPlugin(Plugin):
200
207
  raise RuntimeError("No piped input from stdin")
201
208
  return sys.stdin.read()
202
209
 
203
- @tool
210
+ @tool(
211
+ metadata={
212
+ "banner": code_preview_banner(
213
+ 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]",
216
+ )
217
+ }
218
+ )
204
219
  def exec(
205
220
  self,
206
221
  command: Annotated[
@@ -220,15 +235,8 @@ class CLIPlugin(Plugin):
220
235
  cmd = ["bash", "-c", command]
221
236
  return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
222
237
 
223
- # Print the command and explanation
224
- cmd_preview_panel(
225
- title="Run Command",
226
- content=f"[magenta][bold]➜[/bold] [italic]{command}[/italic][/magenta]\n\n[dim]{explanation}[/dim]",
227
- short=f"[magenta][bold]➜[/bold] [italic]{command}[/italic][/magenta]",
228
- )
229
-
230
238
  # Ask for confirmation
231
- if not confirm("Execute this command?"):
239
+ if not (yield UserConsentEvent("Execute this command?")):
232
240
  return {"error": "The user declined to execute the command."}
233
241
 
234
242
  # Execute the command
@@ -245,7 +253,7 @@ class CLIPlugin(Plugin):
245
253
  title = f"[red][bold]✘[/bold] Command Failed [{proc_result.returncode}][/red]"
246
254
  else:
247
255
  title = "[green][bold]✔[/bold] Command Finished[/green]"
248
- cmd_result_panel(title, out, err)
256
+ code_result_panel(title, out, err)
249
257
 
250
258
  result = {
251
259
  "stdout": proc_result.stdout.decode("utf-8"),
@@ -255,10 +263,9 @@ class CLIPlugin(Plugin):
255
263
  }
256
264
  return result
257
265
 
258
- @tool
266
+ @tool(metadata={"banner": simple_banner("EXIT")})
259
267
  def exit(self, exitcode: Annotated[int, "The exit code of this shell session"] = 0):
260
268
  """
261
269
  Exit the current shell session with an optional exit code.
262
270
  """
263
- banner("EXIT", str(exitcode))
264
271
  sys.exit(exitcode)
autosh/plugins/clock.py CHANGED
@@ -2,14 +2,13 @@ import datetime
2
2
  from agentia.plugins import tool, Plugin
3
3
  import tzlocal
4
4
 
5
- from autosh.plugins import banner
5
+ from autosh.plugins import simple_banner
6
6
 
7
7
 
8
8
  class ClockPlugin(Plugin):
9
- @tool
9
+ @tool(metadata={"banner": simple_banner("GET TIME")})
10
10
  def get_current_time(self):
11
11
  """Get the current UTC time in ISO format"""
12
- banner("GET TIME")
13
12
  utc = datetime.datetime.now(datetime.timezone.utc).isoformat()
14
13
  local = datetime.datetime.now().isoformat()
15
14
  timezone = tzlocal.get_localzone_name()
autosh/plugins/code.py CHANGED
@@ -1,15 +1,32 @@
1
- from agentia.plugins import tool, Plugin
1
+ from agentia import tool, Plugin, UserConsentEvent
2
2
  from typing import Annotated
3
3
  import traceback
4
4
  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 confirm, cmd_result_panel, cmd_preview_panel
8
+ from . import code_preview_banner, code_result_panel
9
+
10
+
11
+ @group()
12
+ def code_with_explanation(code: str, explanation: str):
13
+ yield Syntax(code.strip(), "python")
14
+ yield "\n[dim]───[/dim]\n"
15
+ yield f"[dim]{explanation}[/dim]"
9
16
 
10
17
 
11
18
  class CodePlugin(Plugin):
12
- @tool
19
+ @tool(
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(
25
+ a.get("python_code", ""), a.get("explanation", "")
26
+ ),
27
+ )
28
+ }
29
+ )
13
30
  def execute(
14
31
  self,
15
32
  python_code: Annotated[str, "The python code to run."],
@@ -22,20 +39,7 @@ class CodePlugin(Plugin):
22
39
  The python code must be a valid python source file that accepts no inputs.
23
40
  Print results to stdout or stderr.
24
41
  """
25
-
26
- @group()
27
- def code_with_explanation():
28
- yield Syntax(python_code.strip(), "python")
29
- yield "\n[dim]───[/dim]\n"
30
- yield f"[dim]{explanation}[/dim]"
31
-
32
- cmd_preview_panel(
33
- title="Run Python",
34
- content=code_with_explanation(),
35
- short=f"[bold]RUN[/bold] [italic]Python Code[/italic]",
36
- )
37
-
38
- if not confirm("Execute this code?"):
42
+ if not (yield UserConsentEvent("Execute this code?")):
39
43
  return {"error": "The user declined to execute the command."}
40
44
 
41
45
  out = io.StringIO()
@@ -64,5 +68,5 @@ class CodePlugin(Plugin):
64
68
  "traceback": repr(traceback.format_exc()),
65
69
  }
66
70
 
67
- cmd_result_panel(title, o, e)
71
+ code_result_panel(title, o, e)
68
72
  return result
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 banner
6
+ from autosh.plugins import simple_banner
7
7
 
8
8
 
9
9
  class SearchPlugin(Plugin):
@@ -17,7 +17,11 @@ 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
20
+ @tool(
21
+ metadata={
22
+ "banner": simple_banner("WEB SEARCH", dim=lambda a: a.get("query", "")),
23
+ }
24
+ )
21
25
  async def web_search(
22
26
  self,
23
27
  query: Annotated[
@@ -29,8 +33,6 @@ class SearchPlugin(Plugin):
29
33
  Returning the top related search results in json format.
30
34
  When necessary, you need to combine this tool with the get_webpage_content tools (if available), to browse the web in depth by jumping through links.
31
35
  """
32
- banner("WEB SEARCH", dim=query)
33
-
34
36
  tavily_results = self.__tavily.search(
35
37
  query=query,
36
38
  search_depth="advanced",
@@ -41,7 +43,11 @@ class SearchPlugin(Plugin):
41
43
  )
42
44
  return tavily_results
43
45
 
44
- @tool
46
+ @tool(
47
+ metadata={
48
+ "banner": simple_banner("NEWS SEARCH", dim=lambda a: a.get("query", "")),
49
+ }
50
+ )
45
51
  async def news_search(
46
52
  self,
47
53
  query: Annotated[
@@ -52,7 +58,6 @@ class SearchPlugin(Plugin):
52
58
  Perform news search on the given query.
53
59
  Returning the top related results in json format.
54
60
  """
55
- banner("NEWS SEARCH", dim=query)
56
61
 
57
62
  tavily_results = self.__tavily.search(
58
63
  query=query,
@@ -65,7 +70,11 @@ class SearchPlugin(Plugin):
65
70
  )
66
71
  return tavily_results
67
72
 
68
- @tool
73
+ @tool(
74
+ metadata={
75
+ "banner": simple_banner("FINANCE SEARCH", dim=lambda a: a.get("query", ""))
76
+ }
77
+ )
69
78
  async def finance_search(
70
79
  self,
71
80
  query: Annotated[
@@ -76,7 +85,6 @@ class SearchPlugin(Plugin):
76
85
  Search for finance-related news and information on the given query.
77
86
  Returning the top related results in json format.
78
87
  """
79
- banner("FINANCE SEARCH", dim=query)
80
88
 
81
89
  tavily_results = self.__tavily.search(
82
90
  query=query,
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 banner
10
+ from autosh.plugins import simple_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
50
+ @tool(metadata={"banner": simple_banner("BROWSE", dim=lambda a: a.get("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"],
@@ -57,7 +57,6 @@ class WebPlugin(Plugin):
57
57
  You can always use this tool to directly access web content or access external sites.
58
58
  Use it at any time when you think you may need to access the internet.
59
59
  """
60
- banner("BROWSE", dim=url)
61
60
 
62
61
  result = self.__tavily.extract(
63
62
  urls=url,
autosh/session.py CHANGED
@@ -1,17 +1,24 @@
1
1
  import asyncio
2
2
  from pathlib import Path
3
3
  import sys
4
- from agentia import Agent
5
- from agentia.chat_completion import MessageStream
6
- from agentia.message import UserMessage
4
+ from agentia import (
5
+ Agent,
6
+ UserMessage,
7
+ Event,
8
+ ToolCallEvent,
9
+ MessageStream,
10
+ ChatCompletion,
11
+ UserConsentEvent,
12
+ )
7
13
  from agentia.plugins import PluginInitError
14
+ from neongrid.loading import Loading
8
15
 
9
16
  from autosh.config import CLI_OPTIONS, CONFIG
10
- from autosh.md import stream_md
17
+ import neongrid as ng
11
18
  from .plugins import create_plugins
12
19
  import rich
13
20
  import platform
14
- from rich.prompt import Prompt
21
+ from rich.prompt import Confirm
15
22
 
16
23
 
17
24
  INSTRUCTIONS = f"""
@@ -79,16 +86,16 @@ class Session:
79
86
  * -h, --help Show this message and exit.
80
87
  """,
81
88
  )
82
- agent.history.add(self._get_argv_message())
89
+ agent.history.add(self.__get_argv_message())
83
90
  completion = agent.chat_completion(prompt, stream=True)
84
91
  async for stream in completion:
85
92
  await self.__render_streamed_markdown(stream)
86
93
  sys.exit(0)
87
94
 
88
- def _get_argv_message(self):
95
+ def __get_argv_message(self):
89
96
  args = str(CLI_OPTIONS.args)
90
97
  if not CLI_OPTIONS.script:
91
- cmd = "PROMPT"
98
+ cmd = Path(sys.argv[0]).name
92
99
  else:
93
100
  cmd = CLI_OPTIONS.script.name
94
101
  return UserMessage(
@@ -112,21 +119,30 @@ class Session:
112
119
  # Execute the prompt
113
120
  loading = self.__create_loading_indicator()
114
121
  CLI_OPTIONS.prompt = prompt
115
- self.agent.history.add(self._get_argv_message())
122
+ self.agent.history.add(self.__get_argv_message())
116
123
  if CLI_OPTIONS.stdin_has_data():
117
124
  self.agent.history.add(
118
125
  UserMessage(
119
- content="IMPORTANT: The user is using piped stdin to feed additional data to you. Please use tools to read when necessary.",
126
+ content="IMPORTANT: You are acting as an intermediate tool of a workflow. Input data is fed to you through piped stdin. Please use tools to read when necessary.",
120
127
  role="user",
121
128
  )
122
129
  )
123
- completion = self.agent.chat_completion(prompt, stream=True)
124
- async for stream in completion:
125
- if not loading:
126
- loading = self.__create_loading_indicator()
127
- if await self.__render_streamed_markdown(stream, loading=loading):
128
- print()
129
- loading = None
130
+ if not sys.stdout.isatty():
131
+ self.agent.history.add(
132
+ UserMessage(
133
+ content="IMPORTANT: You are acting as an intermediate tool of a workflow. Your output should only contain the user expected output, nothing else. Don't ask user questions or print anything else since the user cannot see it.",
134
+ role="user",
135
+ )
136
+ )
137
+ else:
138
+ self.agent.history.add(
139
+ UserMessage(
140
+ content="IMPORTANT: This is a one-off run, so don't ask user questions since the user cannot reply.",
141
+ role="user",
142
+ )
143
+ )
144
+ completion = self.agent.chat_completion(prompt, stream=True, events=True)
145
+ await self.__process_completion(completion, loading)
130
146
 
131
147
  async def exec_from_stdin(self):
132
148
  if sys.stdin.isatty():
@@ -143,104 +159,65 @@ class Session:
143
159
  prompt = f.read()
144
160
  await self.exec_prompt(prompt)
145
161
 
162
+ async def __process_event(self, e: Event):
163
+ 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:
171
+ 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
178
+
179
+ async def __process_completion(
180
+ self, completion: ChatCompletion[Event | MessageStream], loading: Loading
181
+ ):
182
+ async for stream in completion:
183
+ await loading.finish()
184
+
185
+ if isinstance(stream, Event):
186
+ await self.__process_event(stream)
187
+ else:
188
+ print()
189
+ await self.__render_streamed_markdown(stream)
190
+ print()
191
+
192
+ loading = self.__create_loading_indicator()
193
+
194
+ await loading.finish()
195
+
146
196
  async def run_repl(self):
147
- console = rich.console.Console()
148
197
  while True:
149
198
  try:
150
- prompt = console.input("[bold blue]>[/bold blue] ").strip()
199
+ prompt = (
200
+ await ng.input("> ", sync=False, persist="/tmp/autosh-history")
201
+ ).strip()
151
202
  if prompt in ["exit", "quit"]:
152
203
  break
153
204
  if len(prompt) == 0:
154
205
  continue
155
- loading = self.__create_loading_indicator(newline=True)
156
- completion = self.agent.chat_completion(prompt, stream=True)
157
- async for stream in completion:
158
- if not loading:
159
- loading = self.__create_loading_indicator()
160
- if await self.__render_streamed_markdown(stream, loading=loading):
161
- print()
162
- loading = None
206
+ 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)
163
211
  except KeyboardInterrupt:
164
212
  break
165
213
 
166
- def __create_loading_indicator(self, newline: bool = False):
167
- return (
168
- asyncio.create_task(self.__loading(newline))
169
- if sys.stdout.isatty()
170
- else None
171
- )
214
+ def __create_loading_indicator(self):
215
+ return ng.loading.kana()
172
216
 
173
- async def __loading(self, newline: bool = False):
174
- chars = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
175
- char_width = 1
176
- msg = "Loading..."
177
- count = 0
178
- print("\x1b[2m", end="", flush=True)
179
- while True:
180
- try:
181
- print(chars[count], end="", flush=True)
182
- print(" " + msg, end="", flush=True)
183
- count += 1
184
- await asyncio.sleep(0.1)
185
- length = char_width + len(msg) + 1
186
- print("\b" * length, end="", flush=True)
187
- print(" " * length, end="", flush=True)
188
- print("\b" * length, end="", flush=True)
189
- if count == len(chars):
190
- count = 0
191
- except asyncio.CancelledError:
192
- length = char_width + len(msg) + 1
193
- print("\b" * length, end="", flush=True)
194
- print(" " * length, end="", flush=True)
195
- print("\b" * length, end="", flush=True)
196
- print("\x1b[0m", end="", flush=True)
197
- if newline:
198
- print()
199
- break
200
-
201
- async def __render_streamed_markdown(
202
- self, stream: MessageStream, loading: asyncio.Task[None] | None = None
203
- ):
217
+ async def __render_streamed_markdown(self, stream: MessageStream):
204
218
  if sys.stdout.isatty():
205
- # buffer first few chars so we don't need to launch glow if there is no output
206
- chunks = aiter(stream)
207
- buf = ""
208
- while len(buf) < 8:
209
- try:
210
- buf += await anext(chunks)
211
- except StopAsyncIteration:
212
- if len(buf) == 0:
213
- if loading:
214
- loading.cancel()
215
- await loading
216
- return False
217
- break
218
- if loading:
219
- loading.cancel()
220
- await loading
221
-
222
- content = {"v": ""}
223
-
224
- async def gen():
225
- content["v"] = buf
226
- if buf:
227
- yield buf
228
- while True:
229
- try:
230
- s = await anext(chunks)
231
- content["v"] += s
232
- for c in s:
233
- yield c
234
- except StopAsyncIteration:
235
- break
236
-
237
- await stream_md(gen())
219
+ await ng.stream.markdown(aiter(stream))
238
220
  return True
239
221
  else:
240
- has_content = False
241
222
  async for chunk in stream:
242
- if chunk == "":
243
- continue
244
- has_content = True
245
223
  print(chunk, end="", flush=True)
246
- return has_content
@@ -1,12 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autosh
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Add your description here
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.13
7
- Requires-Dist: agentia>=0.0.5
7
+ Requires-Dist: agentia>=0.0.7
8
8
  Requires-Dist: asyncio>=3.4.3
9
9
  Requires-Dist: markdownify>=1.1.0
10
+ Requires-Dist: neongrid
11
+ Requires-Dist: prompt-toolkit>=3.0.51
10
12
  Requires-Dist: pydantic>=2.11.3
11
13
  Requires-Dist: python-dotenv>=1.1.0
12
14
  Requires-Dist: rich>=14.0.0
@@ -31,7 +33,9 @@ As an interactive shell: `ash` (alternatively, `autosh`)
31
33
 
32
34
  Execute a single prompt: `ash "list current directory"`
33
35
 
34
- Process piped data: `cat README.md | ash "summarise"`
36
+ Process piped data:
37
+ * `cat README.md | ash -y "summarise"`
38
+ * `cat in.csv | ash -y -q "double the first numeric column" > out.csv`
35
39
 
36
40
  ## Scripting
37
41
 
@@ -72,6 +76,8 @@ Write "Hello, world" to _test.log
72
76
 
73
77
  # TODO
74
78
 
75
- - [ ] Image generation
76
- - [ ] Image input
79
+ - [ ] Image input, generation, and editing
77
80
  - [ ] RAG for non-text files
81
+ - [ ] Plugin system
82
+ - [ ] MCP support
83
+ - [ ] A better 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=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.4.dist-info/METADATA,sha256=cv69lLqKHEFwz5AZWEmmL0scvhzYv8-ECPdEiRzOaus,1959
14
+ autosh-0.0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
+ autosh-0.0.4.dist-info/entry_points.txt,sha256=BV7bzUnxG6Z5InEkrfajGCxjooYORC5tZDDZctOPenQ,67
16
+ autosh-0.0.4.dist-info/licenses/LICENSE,sha256=BnLDJsIJe-Dm18unR9DOoSv7QOfAz6LeIQc1yHAjxp0,1066
17
+ autosh-0.0.4.dist-info/RECORD,,
autosh/md.py DELETED
@@ -1,408 +0,0 @@
1
- import os
2
- from typing import AsyncGenerator, Literal
3
-
4
-
5
- HIGHLIGHT_COLOR_START = "35"
6
- HIGHLIGHT_COLOR_END = "0"
7
-
8
-
9
- class MarkdowmPrinter:
10
- def __init__(self, stream: AsyncGenerator[str, None]):
11
- async def char_stream(stream: AsyncGenerator[str, None]):
12
- async for chunk in stream:
13
- for char in chunk:
14
- yield char
15
-
16
- self.stream = char_stream(stream)
17
- self.__buf: str = ""
18
- self.__eof = False
19
-
20
- def peek(self):
21
- c = self.__buf[0] if len(self.__buf) > 0 else None
22
- return c
23
-
24
- async def __ensure_length(self, n: int):
25
- while (not self.__eof) and len(self.__buf) < n:
26
- try:
27
- self.__buf += await self.stream.__anext__()
28
- except StopAsyncIteration:
29
- self.__eof = True
30
-
31
- async def __check_unordered_list_label(self) -> bool:
32
- if self.__eof:
33
- return False
34
- await self.__ensure_length(2)
35
- buf = self.__buf
36
- if len(buf) < 2:
37
- return False
38
- if buf[0] in ["-", "+", "*"] and buf[1] == " ":
39
- return True
40
- return False
41
-
42
- async def __check_ordered_list_label(self) -> bool:
43
- if self.__eof:
44
- return False
45
- await self.__ensure_length(5)
46
- buf = self.__buf
47
- # \d+\.
48
- if len(buf) == 0:
49
- return False
50
- if not buf[0].isnumeric():
51
- return False
52
- has_dot = False
53
- for i in range(1, 5):
54
- if i >= len(buf):
55
- return False
56
- c = buf[i]
57
- if c == ".":
58
- if has_dot:
59
- return False
60
- has_dot = True
61
- continue
62
- if c == " ":
63
- if has_dot:
64
- return True
65
- return False
66
- if c.isnumeric():
67
- continue
68
- return False
69
-
70
- async def check(self, s: str):
71
- if len(s) == 0:
72
- return True
73
- await self.__ensure_length(len(s))
74
- if len(self.__buf) < len(s):
75
- return False
76
- return self.__buf[0 : len(s)] == s
77
-
78
- async def check_non_paragraph_block_start(self):
79
- await self.__ensure_length(3)
80
- buf = self.__buf[:3] if len(self.__buf) >= 3 else self.__buf
81
- if buf.startswith("```"):
82
- return True
83
- if buf.startswith("---"):
84
- return True
85
- if buf.startswith("> "):
86
- return True
87
- if await self.__check_ordered_list_label():
88
- return True
89
- if await self.__check_unordered_list_label():
90
- return True
91
- return False
92
-
93
- async def next(self):
94
- c = self.__buf[0] if len(self.__buf) > 0 else None
95
- self.__buf = self.__buf[1:] if len(self.__buf) > 0 else ""
96
- if c is None:
97
- return None
98
- if not self.__eof:
99
- try:
100
- self.__buf += await self.stream.__anext__()
101
- except StopAsyncIteration:
102
- self.__eof = True
103
- return c
104
-
105
- def print(self, s: str):
106
- print(s, end="", flush=True)
107
-
108
- async def parse_single_line_text(
109
- self,
110
- outer_is_italic: bool = False,
111
- outer_is_bold: bool = False,
112
- outer_is_dim: bool = False,
113
- ):
114
- styles: list[Literal["code", "bold", "italic", "strike"]] = []
115
-
116
- def find(s: Literal["code", "bold", "italic", "strike"]):
117
- for i in range(len(styles) - 1, -1, -1):
118
- if styles[i] == s:
119
- return i
120
- return None
121
-
122
- def find_italic_first():
123
- for i in range(len(styles) - 1, -1, -1):
124
- if styles[i] == "bold":
125
- return False
126
- if styles[i] == "italic":
127
- return True
128
- return False
129
-
130
- # Remove leading spaces
131
- while self.peek() in [" ", "\t"]:
132
- await self.next()
133
-
134
- while True:
135
- not_code = find("code") is None
136
- c = self.peek()
137
- if c == "\n" or c is None:
138
- await self.next()
139
- self.print("\x1b[0m\n") # Reset all and newline
140
- return
141
- match c:
142
- case "`":
143
- await self.next()
144
- if (i := find("code")) is not None:
145
- self.print(c)
146
- if not outer_is_dim:
147
- self.print("\x1b[22m")
148
- if find("bold") is not None or outer_is_bold:
149
- self.print("\x1b[1m")
150
- styles = styles[:i]
151
- else:
152
- self.print("\x1b[2m")
153
- styles.append("code")
154
- self.print(c)
155
- # Bold
156
- case "*" if (
157
- not_code and await self.check("**") and not find_italic_first()
158
- ):
159
- await self.next()
160
- await self.next()
161
- # print(">", styles, find("bold"))
162
- if (i := find("bold")) is not None:
163
- self.print(c)
164
- self.print(c)
165
- if not outer_is_bold:
166
- self.print("\x1b[22m")
167
- styles = styles[:i]
168
- else:
169
- self.print("\x1b[1m")
170
- styles.append("bold")
171
- self.print(c)
172
- self.print(c)
173
- case "_" if (
174
- not_code and await self.check("__") and not find_italic_first()
175
- ):
176
- await self.next()
177
- await self.next()
178
- if (i := find("bold")) is not None:
179
- self.print(c)
180
- self.print(c)
181
- if not outer_is_bold:
182
- self.print("\x1b[22m")
183
- styles = styles[:i]
184
- else:
185
- self.print("\x1b[1m")
186
- styles.append("bold")
187
- self.print(c)
188
- self.print(c)
189
- # Italic
190
- case "*" | "_" if (
191
- not_code
192
- and not await self.check("* ")
193
- and not await self.check("_ ")
194
- ):
195
- await self.next()
196
- if (i := find("italic")) is not None:
197
- self.print(c)
198
- if not outer_is_italic:
199
- self.print("\x1b[23m")
200
- styles = styles[:i]
201
- # print(styles, await self.check("**"))
202
- else:
203
- self.print("\x1b[3m")
204
- styles.append("italic")
205
- self.print(c)
206
- # Strike through
207
- case "~" if not_code and await self.check("~~"):
208
- await self.next()
209
- await self.next()
210
- if (i := find("strike")) is not None:
211
- self.print("~~")
212
- self.print("\x1b[29m")
213
- styles = styles[:i]
214
- else:
215
- self.print("\x1b[9m")
216
- styles.append("strike")
217
- self.print("~~")
218
- case _:
219
- self.print(c)
220
- await self.next()
221
-
222
- async def parse_heading(self):
223
- hashes = 0
224
- while True:
225
- c = await self.next()
226
- if c == "#":
227
- hashes += 1
228
- else:
229
- break
230
- # Start control
231
- match hashes:
232
- case 1:
233
- self.print("\x1b[45;1;2m") # Magenta background, bold, dim
234
- self.print("#" * hashes)
235
- self.print(" \x1b[22;1m") # Reset dim
236
- await self.parse_single_line_text(outer_is_bold=True)
237
- case 2:
238
- self.print("\x1b[35;1;2;4m") # Magenta foreground, bold, dim, underline
239
- self.print("#" * hashes)
240
- self.print(" \x1b[22m\x1b[1m") # Reset dim
241
- await self.parse_single_line_text(outer_is_bold=True)
242
- case 3:
243
- self.print("\x1b[35;1;2m") # Magenta foreground, bold, dim
244
- self.print("#" * hashes)
245
- self.print(" \x1b[22m\x1b[1m") # Reset dim
246
- await self.parse_single_line_text(outer_is_bold=True)
247
- case 4:
248
- self.print("\x1b[35;2;3m") # Magenta foreground, dim, italic
249
- self.print("#" * hashes)
250
- self.print(" \x1b[22m") # Reset dim
251
- await self.parse_single_line_text(outer_is_italic=True)
252
- case _:
253
- self.print("\x1b[2m") # dim
254
- self.print("#" * hashes)
255
- self.print(" \x1b[22m") # Reset dim
256
- await self.parse_single_line_text()
257
- # Stream title
258
-
259
- async def parse_paragraph(self):
260
- while True:
261
- await self.parse_single_line_text()
262
- if self.peek() != "\n" and not await self.check_non_paragraph_block_start():
263
- await self.next()
264
- break
265
- else:
266
- break
267
-
268
- async def parse_multiline_code(self):
269
- # dim
270
- self.print("\x1b[2m")
271
- self.print("```")
272
- await self.next()
273
- await self.next()
274
- await self.next()
275
- while not await self.check("\n```"):
276
- c = await self.next()
277
- if c is None:
278
- self.print("\n")
279
- return
280
- self.print(c)
281
- self.print("\n```\n")
282
- await self.next()
283
- await self.next()
284
- await self.next()
285
- await self.next()
286
-
287
- async def parse_list(self, ordered: bool):
288
- indents = [0]
289
- counter = [1]
290
- # first item
291
- if ordered:
292
- self.print("1. ")
293
- await self.next()
294
- else:
295
- self.print("• ")
296
- await self.next()
297
- await self.parse_single_line_text()
298
- while True:
299
- indent = 0
300
- while self.peek() in [" ", "\t", "\n"]:
301
- if self.peek() in [" ", "\t"]:
302
- indent += 1
303
- if self.peek() == "\n":
304
- indent = 0
305
- await self.next()
306
- if self.peek() is None:
307
- return
308
- if ordered and not await self.__check_ordered_list_label():
309
- return
310
- if not ordered and not await self.__check_unordered_list_label():
311
- return
312
- if not ordered:
313
- await self.next()
314
- else:
315
- while self.peek() is not None and self.peek() != ".":
316
- await self.next()
317
- await self.next()
318
-
319
- depth = None
320
- for i in range(len(indents) - 1):
321
- if indents[i] <= indent and indents[i + 1] > indent:
322
- depth = i
323
- break
324
- if depth is None and indents[-1] + 2 <= indent:
325
- # indent one more level
326
- indents.append(indent)
327
- depth = len(indents) - 1
328
- counter.append(1)
329
- elif depth is None:
330
- # same as last level
331
- depth = len(indents) - 1
332
- counter[depth] += 1
333
- else:
334
- # dedent
335
- indents = indents[: depth + 1]
336
- counter = counter[: depth + 1]
337
- counter[depth] += 1
338
- if not ordered:
339
- self.print(" " * depth + "• ")
340
- else:
341
- self.print(" " * depth + str(counter[depth]) + ". ")
342
- await self.parse_single_line_text()
343
-
344
- async def parse_blockquote(self):
345
- while True:
346
- while self.peek() in [" ", "\t"]:
347
- await self.next()
348
- if self.peek() != ">":
349
- break
350
- await self.next()
351
- self.print("\x1b[1;2m|\x1b[22;2m ")
352
- await self.parse_single_line_text(outer_is_dim=True)
353
-
354
- async def parse_doc(self):
355
- self.__buf = await self.stream.__anext__()
356
- start = True
357
- while True:
358
- # Remove leading spaces and empty lines
359
- indent = 0
360
- while self.peek() in [" ", "\t", "\n"]:
361
- if self.peek() in [" ", "\t"]:
362
- indent += 1
363
- if self.peek() == "\n":
364
- indent = 0
365
- await self.next()
366
- if self.peek() is None:
367
- break
368
- if not start:
369
- self.print("\n")
370
- start = False
371
- match c := self.peek():
372
- case None:
373
- break
374
- # Heading
375
- case "#":
376
- await self.parse_heading()
377
- # Code
378
- case "`" if await self.check("```"):
379
- await self.parse_multiline_code()
380
- # Separator
381
- case _ if await self.check("---"):
382
- await self.next()
383
- await self.next()
384
- await self.next()
385
- width = min(os.get_terminal_size().columns, 80)
386
- self.print("\x1b[2m" + "─" * width + "\x1b[22m\n")
387
- # Unordered list
388
- case _ if await self.__check_unordered_list_label():
389
- await self.parse_list(False)
390
- # Ordered list
391
- case _ if await self.__check_ordered_list_label():
392
- await self.parse_list(True)
393
- # Blockquote
394
- case ">":
395
- await self.parse_blockquote()
396
- # Normal paragraph
397
- case _:
398
- await self.parse_paragraph()
399
- self.print("\x1b[0m\x1b[0m\x1b[0m") # Reset all
400
- self.print("\x1b[0m") # Reset all
401
-
402
- def __await__(self):
403
- return self.parse_doc().__await__()
404
-
405
-
406
- async def stream_md(stream: AsyncGenerator[str, None]):
407
- mp = MarkdowmPrinter(stream)
408
- await mp.parse_doc()
@@ -1,17 +0,0 @@
1
- autosh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- autosh/config.py,sha256=KPuXr_MF5Pbdn5pCCgq9AcyDSDlOnCJ4vBetKmhodic,2348
3
- autosh/main.py,sha256=myRolKqfHyQEgujqmUndDSm9J0C21w3zqA9-SYNX1kY,4961
4
- autosh/md.py,sha256=qXg5ZFVUwek3rUXb-oEti1soRRbIq8EbDq88lEYRoO4,14267
5
- autosh/session.py,sha256=kCaD-7tZpozz56sWlMGp0njqwYRf3cZOau1ulWRKJEI,8885
6
- autosh/plugins/__init__.py,sha256=yOTobuyYFpUWl5BCowGzJG8rZX2Whlagail_QyHVlo4,2487
7
- autosh/plugins/calc.py,sha256=qo0EajIpNPv9PtLNLygyEjVaxo1F6_S62kmoJZq5oLM,581
8
- autosh/plugins/cli.py,sha256=D6S_QHPmjBBB9gwgXeJrwxUs3u0TNty_tHVICbEPGbs,8522
9
- autosh/plugins/clock.py,sha256=GGi0HAG6f6-FP1qqGoyCcUj11q_VnkaGArumsMk0CkY,542
10
- autosh/plugins/code.py,sha256=0JwFzq6ejgbisCqBm_RG1r1WEVNou64ue-siVIpvZqs,2291
11
- autosh/plugins/search.py,sha256=1d3Gqq6uXu0ntTBpw44Ab_haAySvZLMj3e2MQd3DHO0,2736
12
- autosh/plugins/web.py,sha256=lmD2JnsqVI1qKgSFrk39851jCZoPyPRaVvHeEFYXylA,2597
13
- autosh-0.0.2.dist-info/METADATA,sha256=y9pbK71nuueA11LCv6AsR24MSsnqNut-7-zljpOC5_Y,1720
14
- autosh-0.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
- autosh-0.0.2.dist-info/entry_points.txt,sha256=BV7bzUnxG6Z5InEkrfajGCxjooYORC5tZDDZctOPenQ,67
16
- autosh-0.0.2.dist-info/licenses/LICENSE,sha256=BnLDJsIJe-Dm18unR9DOoSv7QOfAz6LeIQc1yHAjxp0,1066
17
- autosh-0.0.2.dist-info/RECORD,,
File without changes