autosh 0.0.1__py3-none-any.whl → 0.0.3__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/md/state.py ADDED
@@ -0,0 +1,136 @@
1
+ from dataclasses import dataclass
2
+ from contextlib import contextmanager
3
+ from enum import Enum
4
+
5
+ HIGHLIGHT_COLOR_START = "35"
6
+ HIGHLIGHT_COLOR_END = "0"
7
+ ESC = "\x1b"
8
+
9
+
10
+ class Color(Enum):
11
+ BLACK = 30, 40
12
+ RED = 31, 41
13
+ GREEN = 32, 42
14
+ YELLOW = 33, 43
15
+ BLUE = 34, 44
16
+ MAGENTA = 35, 45
17
+ CYAN = 36, 46
18
+ WHITE = 37, 47
19
+
20
+ BRIGHT_BLACK = 90, 100
21
+ BRIGHT_RED = 91, 101
22
+ BRIGHT_GREEN = 92, 102
23
+ BRIGHT_YELLOW = 93, 103
24
+ BRIGHT_BLUE = 94, 104
25
+ BRIGHT_MAGENTA = 95, 105
26
+ BRIGHT_CYAN = 96, 106
27
+ BRIGHT_WHITE = 97, 107
28
+
29
+ def __init__(self, foreground: int, background: int):
30
+ self.foreground = foreground
31
+ self.background = background
32
+
33
+
34
+ @dataclass
35
+ class State:
36
+ is_bold: bool = False
37
+ is_dim: bool = False
38
+ is_italic: bool = False
39
+ is_underline: bool = False
40
+ is_strike: bool = False
41
+ foreground_color: Color | None = None
42
+ background_color: Color | None = None
43
+
44
+ def emit(self, text: str):
45
+ print(text, end="", flush=True)
46
+
47
+ def _enter_scope(
48
+ self,
49
+ bold: bool | None = None,
50
+ dim: bool | None = None,
51
+ italic: bool | None = None,
52
+ underline: bool | None = None,
53
+ strike: bool | None = None,
54
+ color: Color | None = None,
55
+ bg: Color | None = None,
56
+ ) -> "State":
57
+ old_state = State(
58
+ is_bold=self.is_bold,
59
+ is_dim=self.is_dim,
60
+ is_italic=self.is_italic,
61
+ is_underline=self.is_underline,
62
+ is_strike=self.is_strike,
63
+ foreground_color=self.foreground_color,
64
+ background_color=self.background_color,
65
+ )
66
+ if bold is not None:
67
+ self.is_bold = bold
68
+ if dim is not None:
69
+ self.is_dim = dim
70
+ if italic is not None:
71
+ self.is_italic = italic
72
+ if underline is not None:
73
+ self.is_underline = underline
74
+ if strike is not None:
75
+ self.is_strike = strike
76
+ if color is not None:
77
+ self.foreground_color = color
78
+ if bg is not None:
79
+ self.background_color = bg
80
+ self.__apply_all()
81
+ return old_state
82
+
83
+ def _exit_scope(self, old_state: "State"):
84
+ self.is_bold = old_state.is_bold
85
+ self.is_dim = old_state.is_dim
86
+ self.is_italic = old_state.is_italic
87
+ self.is_underline = old_state.is_underline
88
+ self.is_strike = old_state.is_strike
89
+ self.foreground_color = old_state.foreground_color
90
+ self.background_color = old_state.background_color
91
+ self.__apply_all()
92
+
93
+ @contextmanager
94
+ def style(
95
+ self,
96
+ bold: bool | None = None,
97
+ dim: bool | None = None,
98
+ italic: bool | None = None,
99
+ underline: bool | None = None,
100
+ strike: bool | None = None,
101
+ color: Color | None = None,
102
+ bg: Color | None = None,
103
+ ):
104
+ old_state = self._enter_scope(
105
+ bold=bold,
106
+ dim=dim,
107
+ italic=italic,
108
+ underline=underline,
109
+ strike=strike,
110
+ color=color,
111
+ bg=bg,
112
+ )
113
+ yield
114
+ self._exit_scope(old_state)
115
+
116
+ def __apply_all(self):
117
+ self.emit(f"{ESC}[0m")
118
+ codes = []
119
+ if self.is_bold:
120
+ codes.append(1)
121
+ if self.is_dim:
122
+ codes.append(2)
123
+ if self.is_italic:
124
+ codes.append(3)
125
+ if self.is_underline:
126
+ codes.append(4)
127
+ if self.is_strike:
128
+ codes.append(9)
129
+ if self.foreground_color:
130
+ codes.append(self.foreground_color.foreground)
131
+ if self.background_color:
132
+ codes.append(self.background_color.background)
133
+
134
+ codes = ";".join(map(str, codes))
135
+ if len(codes) > 0:
136
+ self.emit(f"{ESC}[{codes}m")
autosh/md/stream.py ADDED
@@ -0,0 +1,107 @@
1
+ from typing import AsyncGenerator
2
+
3
+
4
+ class TextStream:
5
+ def __init__(self, gen: AsyncGenerator[str, None]):
6
+ self.__stream = gen
7
+ self.__buf: str = ""
8
+ self.__eof = False
9
+
10
+ async def init(self):
11
+ try:
12
+ self.__buf = await anext(self.__stream)
13
+ except StopAsyncIteration:
14
+ self.__eof = True
15
+
16
+ async def __ensure_length(self, n: int):
17
+ while (not self.__eof) and len(self.__buf) < n:
18
+ try:
19
+ self.__buf += await anext(self.__stream)
20
+ except StopAsyncIteration:
21
+ self.__eof = True
22
+
23
+ def peek(self):
24
+ c = self.__buf[0] if len(self.__buf) > 0 else None
25
+ return c
26
+
27
+ async def check(self, s: str, eof: bool | None = None) -> bool:
28
+ if len(s) == 0:
29
+ return True
30
+ await self.__ensure_length(len(s) + 1)
31
+ if len(self.__buf) < len(s):
32
+ return False
33
+ matched = self.__buf[0 : len(s)] == s
34
+ if matched:
35
+ if eof is not None:
36
+ if eof:
37
+ # return false if there is more data
38
+ if len(self.__buf) > len(s):
39
+ return False
40
+ else:
41
+ # return false if there is no more data
42
+ if len(self.__buf) == len(s):
43
+ return False
44
+ return matched
45
+
46
+ async def consume(self, n: int = 1):
47
+ await self.__ensure_length(n)
48
+ if len(self.__buf) < n:
49
+ raise ValueError("Not enough data to consume")
50
+ s = self.__buf[:n]
51
+ self.__buf = self.__buf[n:]
52
+ await self.__ensure_length(1)
53
+ return s
54
+
55
+ async def unordered_list_label(self) -> bool:
56
+ if self.__eof:
57
+ return False
58
+ await self.__ensure_length(2)
59
+ buf = self.__buf
60
+ if len(buf) < 2:
61
+ return False
62
+ if buf[0] in ["-", "+", "*"] and buf[1] == " ":
63
+ return True
64
+ return False
65
+
66
+ async def ordered_list_label(self) -> bool:
67
+ if self.__eof:
68
+ return False
69
+ await self.__ensure_length(5)
70
+ buf = self.__buf
71
+ # \d+\.
72
+ if len(buf) == 0:
73
+ return False
74
+ if not buf[0].isnumeric():
75
+ return False
76
+ has_dot = False
77
+ for i in range(1, 5):
78
+ if i >= len(buf):
79
+ return False
80
+ c = buf[i]
81
+ if c == ".":
82
+ if has_dot:
83
+ return False
84
+ has_dot = True
85
+ continue
86
+ if c == " ":
87
+ if has_dot:
88
+ return True
89
+ return False
90
+ if c.isnumeric():
91
+ continue
92
+ return False
93
+
94
+ async def non_paragraph_block_start(self):
95
+ await self.__ensure_length(3)
96
+ buf = self.__buf[:3] if len(self.__buf) >= 3 else self.__buf
97
+ if buf.startswith("```"):
98
+ return True
99
+ if buf.startswith("---"):
100
+ return True
101
+ if buf.startswith("> "):
102
+ return True
103
+ if await self.ordered_list_label():
104
+ return True
105
+ if await self.unordered_list_label():
106
+ return True
107
+ return False
@@ -29,8 +29,8 @@ def confirm(message: str):
29
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:
32
+ if CLI_OPTIONS.quiet:
33
+ if short and not CLI_OPTIONS.yes:
34
34
  rich.print(f"[magenta]{short}[/magenta]\n")
35
35
  return
36
36
  panel = Panel.fit(content, title=f"[magenta]{title}[/magenta]", title_align="left")
autosh/plugins/cli.py CHANGED
@@ -82,6 +82,48 @@ class CLIPlugin(Plugin):
82
82
  "args": CLI_OPTIONS.args,
83
83
  }
84
84
 
85
+ @tool
86
+ def get_env(self, key: Annotated[str, "The environment variable to get"]):
87
+ """
88
+ Get an environment variable.
89
+ """
90
+ banner("GET ENV", key)
91
+ if key not in os.environ:
92
+ raise KeyError(f"Environment variable `{key}` does not exist.")
93
+ return os.environ[key]
94
+
95
+ @tool
96
+ def get_all_envs(self):
97
+ """
98
+ Get all environment variables.
99
+ """
100
+ banner("GET ALL ENVS")
101
+ envs = {}
102
+ for key, value in os.environ.items():
103
+ envs[key] = value
104
+ return {"envs": envs}
105
+
106
+ @tool
107
+ def update_env(
108
+ self,
109
+ key: Annotated[str, "The environment variable to set"],
110
+ value: Annotated[
111
+ str | None,
112
+ "The value to set the environment variable to, or None to delete it",
113
+ ],
114
+ ):
115
+ """
116
+ Set or delete an environment variable.
117
+ """
118
+ if value is None:
119
+ banner("DEL ENV", key)
120
+ if key in os.environ:
121
+ del os.environ[key]
122
+ else:
123
+ banner("SET ENV", key, value)
124
+ os.environ[key] = value
125
+ return f"DONE"
126
+
85
127
  @tool
86
128
  def read(
87
129
  self,
autosh/session.py CHANGED
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  from pathlib import Path
2
3
  import sys
3
4
  from agentia import Agent
@@ -10,6 +11,7 @@ from autosh.md import stream_md
10
11
  from .plugins import create_plugins
11
12
  import rich
12
13
  import platform
14
+ from rich.prompt import Prompt
13
15
 
14
16
 
15
17
  INSTRUCTIONS = f"""
@@ -86,11 +88,11 @@ class Session:
86
88
  def _get_argv_message(self):
87
89
  args = str(CLI_OPTIONS.args)
88
90
  if not CLI_OPTIONS.script:
89
- cmd = "PROMPT"
91
+ cmd = Path(sys.argv[0]).name
90
92
  else:
91
93
  cmd = CLI_OPTIONS.script.name
92
94
  return UserMessage(
93
- content=f"PROGRAM NAME: {cmd}\n\nCOMMAND LINE ARGS: {args}",
95
+ content=f"PROGRAM NAME: {cmd}\n\nCOMMAND LINE ARGS: {args}\n\nCWD: {str(Path.cwd())}",
94
96
  role="user",
95
97
  )
96
98
 
@@ -108,19 +110,37 @@ class Session:
108
110
  ):
109
111
  await self._print_help_and_exit(prompt)
110
112
  # Execute the prompt
113
+ loading = self.__create_loading_indicator()
111
114
  CLI_OPTIONS.prompt = prompt
112
115
  self.agent.history.add(self._get_argv_message())
113
116
  if CLI_OPTIONS.stdin_has_data():
114
117
  self.agent.history.add(
115
118
  UserMessage(
116
- content="IMPORTANT: The user is using piped stdin to feed additional data to you. Please use tools to read when necessary.",
119
+ 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
+ role="user",
121
+ )
122
+ )
123
+ if not sys.stdout.isatty():
124
+ self.agent.history.add(
125
+ UserMessage(
126
+ 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.",
127
+ role="user",
128
+ )
129
+ )
130
+ else:
131
+ self.agent.history.add(
132
+ UserMessage(
133
+ content="IMPORTANT: This is a one-off run, so don't ask user questions since the user cannot reply.",
117
134
  role="user",
118
135
  )
119
136
  )
120
137
  completion = self.agent.chat_completion(prompt, stream=True)
121
138
  async for stream in completion:
122
- if await self.__render_streamed_markdown(stream):
139
+ if not loading:
140
+ loading = self.__create_loading_indicator()
141
+ if await self.__render_streamed_markdown(stream, loading=loading):
123
142
  print()
143
+ loading = None
124
144
 
125
145
  async def exec_from_stdin(self):
126
146
  if sys.stdin.isatty():
@@ -141,19 +161,60 @@ class Session:
141
161
  console = rich.console.Console()
142
162
  while True:
143
163
  try:
144
- prompt = console.input("[bold]>[/bold] ").strip()
164
+ prompt = console.input("[bold blue]>[/bold blue] ").strip()
145
165
  if prompt in ["exit", "quit"]:
146
166
  break
147
167
  if len(prompt) == 0:
148
168
  continue
169
+ loading = self.__create_loading_indicator(newline=True)
149
170
  completion = self.agent.chat_completion(prompt, stream=True)
150
171
  async for stream in completion:
151
- if await self.__render_streamed_markdown(stream):
172
+ if not loading:
173
+ loading = self.__create_loading_indicator()
174
+ if await self.__render_streamed_markdown(stream, loading=loading):
152
175
  print()
176
+ loading = None
153
177
  except KeyboardInterrupt:
154
178
  break
155
179
 
156
- async def __render_streamed_markdown(self, stream: MessageStream):
180
+ def __create_loading_indicator(self, newline: bool = False):
181
+ return (
182
+ asyncio.create_task(self.__loading(newline))
183
+ if sys.stdout.isatty()
184
+ else None
185
+ )
186
+
187
+ async def __loading(self, newline: bool = False):
188
+ chars = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
189
+ char_width = 1
190
+ msg = "Loading..."
191
+ count = 0
192
+ print("\x1b[2m", end="", flush=True)
193
+ while True:
194
+ try:
195
+ print(chars[count], end="", flush=True)
196
+ print(" " + msg, end="", flush=True)
197
+ count += 1
198
+ await asyncio.sleep(0.1)
199
+ length = char_width + len(msg) + 1
200
+ print("\b" * length, end="", flush=True)
201
+ print(" " * length, end="", flush=True)
202
+ print("\b" * length, end="", flush=True)
203
+ if count == len(chars):
204
+ count = 0
205
+ except asyncio.CancelledError:
206
+ length = char_width + len(msg) + 1
207
+ print("\b" * length, end="", flush=True)
208
+ print(" " * length, end="", flush=True)
209
+ print("\b" * length, end="", flush=True)
210
+ print("\x1b[0m", end="", flush=True)
211
+ if newline:
212
+ print()
213
+ break
214
+
215
+ async def __render_streamed_markdown(
216
+ self, stream: MessageStream, loading: asyncio.Task[None] | None = None
217
+ ):
157
218
  if sys.stdout.isatty():
158
219
  # buffer first few chars so we don't need to launch glow if there is no output
159
220
  chunks = aiter(stream)
@@ -163,8 +224,14 @@ class Session:
163
224
  buf += await anext(chunks)
164
225
  except StopAsyncIteration:
165
226
  if len(buf) == 0:
227
+ if loading:
228
+ loading.cancel()
229
+ await loading
166
230
  return False
167
231
  break
232
+ if loading:
233
+ loading.cancel()
234
+ await loading
168
235
 
169
236
  content = {"v": ""}
170
237
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autosh
3
- Version: 0.0.1
3
+ Version: 0.0.3
4
4
  Summary: Add your description here
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.13
@@ -31,7 +31,9 @@ As an interactive shell: `ash` (alternatively, `autosh`)
31
31
 
32
32
  Execute a single prompt: `ash "list current directory"`
33
33
 
34
- Process piped data: `cat README.md | ash "summarise"`
34
+ Process piped data:
35
+ * `cat README.md | ash -y "summarise"`
36
+ * `cat in.csv | ash -y -q "double the first numeric column" > out.csv`
35
37
 
36
38
  ## Scripting
37
39
 
@@ -72,6 +74,8 @@ Write "Hello, world" to _test.log
72
74
 
73
75
  # TODO
74
76
 
75
- - [ ] Image generation
76
- - [ ] Image input
77
+ - [ ] Image input, generation, and editing
77
78
  - [ ] RAG for non-text files
79
+ - [ ] Plugin system
80
+ - [ ] MCP support
81
+ - [ ] A better input widget with history and auto completion
@@ -0,0 +1,22 @@
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=ov8lBOUb7GOa3-WBW00gqKaYrTG3NFYQgIXP3-6Eki4,4975
5
+ autosh/session.py,sha256=pU3AAqI3vEEFdtsXK_1ZV6uXzjkzIG8q3Yo01K-Q6dw,9615
6
+ autosh/md/__init__.py,sha256=0SK4rkynUwR77E8IU-ORJmWou_aTxC6xzQQo-IfAsaM,213
7
+ autosh/md/inline_text.py,sha256=I8CaxksvOpTr1mlUtnk50TVVgF9CLnLsth2ux0cdTM4,6422
8
+ autosh/md/printer.py,sha256=qD3AcGosnPMRAf-KiRw1PUixtP-kS5xFIPFjzGLLT0g,11099
9
+ autosh/md/state.py,sha256=OEp0kUb63HrAN5BPcPx9mxWogofayumH9GZGqLdOoEA,3792
10
+ autosh/md/stream.py,sha256=zaVJR6Kog6EZcuVaGszwgywpysHcn8fRZ0KItmKRhiA,3224
11
+ autosh/plugins/__init__.py,sha256=NXq27wvS97NEH6hTtWRyp-ut0BE9fq5QyKBjXOj39zY,2487
12
+ autosh/plugins/calc.py,sha256=qo0EajIpNPv9PtLNLygyEjVaxo1F6_S62kmoJZq5oLM,581
13
+ autosh/plugins/cli.py,sha256=D6S_QHPmjBBB9gwgXeJrwxUs3u0TNty_tHVICbEPGbs,8522
14
+ autosh/plugins/clock.py,sha256=GGi0HAG6f6-FP1qqGoyCcUj11q_VnkaGArumsMk0CkY,542
15
+ autosh/plugins/code.py,sha256=0JwFzq6ejgbisCqBm_RG1r1WEVNou64ue-siVIpvZqs,2291
16
+ autosh/plugins/search.py,sha256=1d3Gqq6uXu0ntTBpw44Ab_haAySvZLMj3e2MQd3DHO0,2736
17
+ autosh/plugins/web.py,sha256=lmD2JnsqVI1qKgSFrk39851jCZoPyPRaVvHeEFYXylA,2597
18
+ autosh-0.0.3.dist-info/METADATA,sha256=M2wVPjOWoiCXWbYzs216oQ4eUSE_s5-b67-qTufmnRE,1897
19
+ autosh-0.0.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
+ autosh-0.0.3.dist-info/entry_points.txt,sha256=BV7bzUnxG6Z5InEkrfajGCxjooYORC5tZDDZctOPenQ,67
21
+ autosh-0.0.3.dist-info/licenses/LICENSE,sha256=BnLDJsIJe-Dm18unR9DOoSv7QOfAz6LeIQc1yHAjxp0,1066
22
+ autosh-0.0.3.dist-info/RECORD,,