autosh 0.0.1__tar.gz → 0.0.2__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.1
3
+ Version: 0.0.2
4
4
  Summary: Add your description here
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.13
@@ -3,6 +3,8 @@ from pydantic import BaseModel, Field
3
3
  from pathlib import Path
4
4
  import tomllib
5
5
 
6
+ import rich
7
+
6
8
  USER_CONFIG_PATH = Path.home() / ".config" / "autosh" / "config.toml"
7
9
 
8
10
 
@@ -42,10 +44,14 @@ class Config(BaseModel):
42
44
  @staticmethod
43
45
  def load() -> "Config":
44
46
  if USER_CONFIG_PATH.is_file():
45
- doc = tomllib.loads(USER_CONFIG_PATH.read_text())
46
- main = doc.get("autosh", {})
47
- plugins = Plugins(**doc.get("plugins", {}))
48
- config = Config.model_validate({**main, "plugins": plugins})
47
+ try:
48
+ doc = tomllib.loads(USER_CONFIG_PATH.read_text())
49
+ main = doc.get("autosh", {})
50
+ plugins = Plugins(**doc.get("plugins", {}))
51
+ config = Config.model_validate({**main, "plugins": plugins})
52
+ except tomllib.TOMLDecodeError as e:
53
+ rich.print(f"[bold red]Error:[/bold red] invalid config file: {e}")
54
+ sys.exit(1)
49
55
  else:
50
56
  config = Config()
51
57
  return config
@@ -142,6 +142,7 @@ class MarkdowmPrinter:
142
142
  case "`":
143
143
  await self.next()
144
144
  if (i := find("code")) is not None:
145
+ self.print(c)
145
146
  if not outer_is_dim:
146
147
  self.print("\x1b[22m")
147
148
  if find("bold") is not None or outer_is_bold:
@@ -150,6 +151,7 @@ class MarkdowmPrinter:
150
151
  else:
151
152
  self.print("\x1b[2m")
152
153
  styles.append("code")
154
+ self.print(c)
153
155
  # Bold
154
156
  case "*" if (
155
157
  not_code and await self.check("**") and not find_italic_first()
@@ -158,24 +160,32 @@ class MarkdowmPrinter:
158
160
  await self.next()
159
161
  # print(">", styles, find("bold"))
160
162
  if (i := find("bold")) is not None:
163
+ self.print(c)
164
+ self.print(c)
161
165
  if not outer_is_bold:
162
166
  self.print("\x1b[22m")
163
167
  styles = styles[:i]
164
168
  else:
165
169
  self.print("\x1b[1m")
166
170
  styles.append("bold")
171
+ self.print(c)
172
+ self.print(c)
167
173
  case "_" if (
168
174
  not_code and await self.check("__") and not find_italic_first()
169
175
  ):
170
176
  await self.next()
171
177
  await self.next()
172
178
  if (i := find("bold")) is not None:
179
+ self.print(c)
180
+ self.print(c)
173
181
  if not outer_is_bold:
174
182
  self.print("\x1b[22m")
175
183
  styles = styles[:i]
176
184
  else:
177
185
  self.print("\x1b[1m")
178
186
  styles.append("bold")
187
+ self.print(c)
188
+ self.print(c)
179
189
  # Italic
180
190
  case "*" | "_" if (
181
191
  not_code
@@ -184,6 +194,7 @@ class MarkdowmPrinter:
184
194
  ):
185
195
  await self.next()
186
196
  if (i := find("italic")) is not None:
197
+ self.print(c)
187
198
  if not outer_is_italic:
188
199
  self.print("\x1b[23m")
189
200
  styles = styles[:i]
@@ -191,16 +202,19 @@ class MarkdowmPrinter:
191
202
  else:
192
203
  self.print("\x1b[3m")
193
204
  styles.append("italic")
205
+ self.print(c)
194
206
  # Strike through
195
207
  case "~" if not_code and await self.check("~~"):
196
208
  await self.next()
197
209
  await self.next()
198
210
  if (i := find("strike")) is not None:
211
+ self.print("~~")
199
212
  self.print("\x1b[29m")
200
213
  styles = styles[:i]
201
214
  else:
202
215
  self.print("\x1b[9m")
203
216
  styles.append("strike")
217
+ self.print("~~")
204
218
  case _:
205
219
  self.print(c)
206
220
  await self.next()
@@ -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,
@@ -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"""
@@ -90,7 +92,7 @@ class Session:
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,6 +110,7 @@ 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():
@@ -119,8 +122,11 @@ class Session:
119
122
  )
120
123
  completion = self.agent.chat_completion(prompt, stream=True)
121
124
  async for stream in completion:
122
- if await self.__render_streamed_markdown(stream):
125
+ if not loading:
126
+ loading = self.__create_loading_indicator()
127
+ if await self.__render_streamed_markdown(stream, loading=loading):
123
128
  print()
129
+ loading = None
124
130
 
125
131
  async def exec_from_stdin(self):
126
132
  if sys.stdin.isatty():
@@ -141,19 +147,60 @@ class Session:
141
147
  console = rich.console.Console()
142
148
  while True:
143
149
  try:
144
- prompt = console.input("[bold]>[/bold] ").strip()
150
+ prompt = console.input("[bold blue]>[/bold blue] ").strip()
145
151
  if prompt in ["exit", "quit"]:
146
152
  break
147
153
  if len(prompt) == 0:
148
154
  continue
155
+ loading = self.__create_loading_indicator(newline=True)
149
156
  completion = self.agent.chat_completion(prompt, stream=True)
150
157
  async for stream in completion:
151
- if await self.__render_streamed_markdown(stream):
158
+ if not loading:
159
+ loading = self.__create_loading_indicator()
160
+ if await self.__render_streamed_markdown(stream, loading=loading):
152
161
  print()
162
+ loading = None
153
163
  except KeyboardInterrupt:
154
164
  break
155
165
 
156
- async def __render_streamed_markdown(self, stream: MessageStream):
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
+ )
172
+
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
+ ):
157
204
  if sys.stdout.isatty():
158
205
  # buffer first few chars so we don't need to launch glow if there is no output
159
206
  chunks = aiter(stream)
@@ -163,8 +210,14 @@ class Session:
163
210
  buf += await anext(chunks)
164
211
  except StopAsyncIteration:
165
212
  if len(buf) == 0:
213
+ if loading:
214
+ loading.cancel()
215
+ await loading
166
216
  return False
167
217
  break
218
+ if loading:
219
+ loading.cancel()
220
+ await loading
168
221
 
169
222
  content = {"v": ""}
170
223
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "autosh"
3
- version = "0.0.1"
3
+ version = "0.0.2"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes