autosh 0.0.2__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.
@@ -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
@@ -8,7 +8,7 @@ from rich.columns import Columns
8
8
  from rich.panel import Panel
9
9
  import argparse
10
10
 
11
- from autosh.config import CLI_OPTIONS, CONFIG
11
+ from autosh.config import CLI_OPTIONS, CONFIG, USER_CONFIG_PATH
12
12
  from .session import Session
13
13
  import sys
14
14
 
@@ -149,14 +149,13 @@ def main():
149
149
  # dotenv.load_dotenv()
150
150
  prompt, args = parse_args()
151
151
 
152
+ if key := os.getenv("OPENROUTER_API_KEY"):
153
+ CONFIG.api_key = key
152
154
  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)
155
+ rich.print(
156
+ 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]"
157
+ )
158
+ sys.exit(1)
160
159
  try:
161
160
  asyncio.run(start_session(prompt, args))
162
161
  except (KeyboardInterrupt, EOFError):
autosh/md/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ from typing import AsyncGenerator
2
+
3
+ from autosh.md.printer import StreamedMarkdownPrinter
4
+
5
+
6
+ async def stream_md(stream: AsyncGenerator[str, None]):
7
+ mp = StreamedMarkdownPrinter(stream)
8
+ await mp.parse_doc()
@@ -0,0 +1,214 @@
1
+ from dataclasses import dataclass, field
2
+ from autosh.md.printer import StreamedMarkdownPrinter
3
+ from autosh.md.state import State
4
+
5
+
6
+ @dataclass
7
+ class Keyword:
8
+ token: str
9
+
10
+ def is_bold(self) -> bool:
11
+ return self.token[0] in ["*", "_"] and len(self.token) == 2
12
+
13
+ def is_italic(self) -> bool:
14
+ return self.token[0] in ["*", "_"] and len(self.token) == 1
15
+
16
+ def is_bold_and_italic(self) -> bool:
17
+ return self.token[0] in ["*", "_"] and len(self.token) > 2
18
+
19
+ def is_bold_or_italic(self) -> bool:
20
+ return self.token[0] in ["*", "_"]
21
+
22
+
23
+ class InvalidState(Exception):
24
+ def __init__(self, token: str):
25
+ super().__init__(token)
26
+ self.token = token
27
+
28
+
29
+ @dataclass
30
+ class InlineScope:
31
+ state: State
32
+
33
+ stack: list[tuple[Keyword, State]] = field(default_factory=list)
34
+
35
+ @property
36
+ def last(self) -> str | None:
37
+ return self.stack[-1][0].token if len(self.stack) > 0 else None
38
+
39
+ @property
40
+ def strike(self) -> bool:
41
+ return self.last == "~~"
42
+
43
+ @property
44
+ def code(self) -> bool:
45
+ return self.last == "`"
46
+
47
+ def enter(self, kind: Keyword):
48
+ match kind:
49
+ case "`":
50
+ state = self.state._enter_scope(dim=True)
51
+ case "~~":
52
+ state = self.state._enter_scope(strike=True)
53
+ case _ if kind.is_bold():
54
+ state = self.state._enter_scope(bold=True)
55
+ case _ if kind.is_italic():
56
+ state = self.state._enter_scope(italic=True)
57
+ case _ if kind.is_bold_and_italic():
58
+ state = self.state._enter_scope(bold=True, italic=True)
59
+ case _:
60
+ state = self.state._enter_scope()
61
+ self.stack.append((kind, state))
62
+
63
+ def exit(self):
64
+ t = self.stack.pop()
65
+ state = t[1]
66
+ self.state._exit_scope(state)
67
+
68
+
69
+ class InlineTextPrinter:
70
+ def __init__(self, p: StreamedMarkdownPrinter, table: bool = False):
71
+ self.p = p
72
+ self.terminator = ["\n", None]
73
+ if table:
74
+ self.terminator.append("|")
75
+
76
+ def emit(self, s: str):
77
+ self.p.emit(s)
78
+
79
+ def peek(self):
80
+ return self.p.peek()
81
+
82
+ async def consume(self, n: int = 1):
83
+ return await self.p.consume(n)
84
+
85
+ async def check(self, s: str, eof: bool | None = None) -> bool:
86
+ return await self.p.check(s, eof)
87
+
88
+ async def parse_inline_unformatted(self):
89
+ # Parse until another "`" or a newline, or EOF
90
+ while not self.peek() in self.terminator:
91
+ c = await self.p.consume()
92
+ self.emit(c)
93
+
94
+ async def parse_inline_code(self):
95
+ self.emit("`")
96
+ # Parse until another "`" or a newline, or EOF
97
+ while not await self.check("`"):
98
+ c = await self.p.consume()
99
+ if c in self.terminator:
100
+ return
101
+ self.emit(c)
102
+ if c == "\\":
103
+ c = await self.p.consume()
104
+ if c in self.terminator:
105
+ return
106
+ self.emit(c)
107
+ self.emit("`")
108
+ await self.p.consume()
109
+
110
+ async def get_next_token(self) -> str | Keyword | None:
111
+ c = self.peek()
112
+ if c in self.terminator:
113
+ return None
114
+ if c == "`":
115
+ await self.consume()
116
+ return Keyword("`")
117
+ if c == "*":
118
+ s = ""
119
+ while self.peek() == "*":
120
+ s += c
121
+ await self.consume()
122
+ return Keyword(s)
123
+ if c == "_":
124
+ s = ""
125
+ while self.peek() == "_":
126
+ s += c
127
+ await self.consume()
128
+ return Keyword(s)
129
+ if c == "~" and await self.check("~~"):
130
+ s = ""
131
+ while self.peek() == "~":
132
+ s += c
133
+ await self.consume()
134
+ return Keyword(s)
135
+ if c == " " or c == "\t":
136
+ s = ""
137
+ while self.peek() == " " or self.peek() == "\t":
138
+ s += c
139
+ await self.consume()
140
+ return s
141
+ s = c or ""
142
+ if c == "\\":
143
+ await self.consume()
144
+ c = self.peek()
145
+ if c in self.terminator:
146
+ return None if len(s) == 0 else s
147
+ s += c or ""
148
+ await self.consume()
149
+ else:
150
+ await self.consume()
151
+ if len(s) == 0:
152
+ return None
153
+ return s
154
+
155
+ async def parse_inline(self, consume_trailing_newline: bool = True):
156
+ last_is_space = False
157
+ start = True
158
+ scope = InlineScope(self.p.state)
159
+
160
+ while True:
161
+ t = await self.get_next_token()
162
+ curr_is_space = False
163
+
164
+ if t is None:
165
+ if consume_trailing_newline:
166
+ if self.peek() == "\n":
167
+ await self.consume()
168
+ self.emit("\n")
169
+ return
170
+ elif isinstance(t, str):
171
+ # Space or normal text
172
+ if t[0] == " ":
173
+ curr_is_space = True
174
+ self.emit(" ")
175
+ else:
176
+ self.emit(t)
177
+ elif t.token == "`":
178
+ # Inline code
179
+ scope.enter(t)
180
+ with self.p.state.style(dim=True):
181
+ await self.parse_inline_code()
182
+ scope.exit()
183
+ elif t.token == "~~":
184
+ # Strike through
185
+ if not scope.strike:
186
+ scope.enter(t)
187
+ self.emit("~~")
188
+ else:
189
+ self.emit("~~")
190
+ scope.exit()
191
+ elif (
192
+ t.is_bold_or_italic()
193
+ and (last_is_space or start)
194
+ and scope.last != t.token
195
+ ):
196
+ # Start bold or italics
197
+ scope.enter(t)
198
+ self.emit(t.token)
199
+ elif (
200
+ t.is_bold_or_italic()
201
+ and self.peek() in [" ", "\t", *self.terminator]
202
+ and scope.last == t.token
203
+ ):
204
+ # End bold or italics
205
+ self.emit(t.token)
206
+ scope.exit()
207
+ else:
208
+ # print(
209
+ # "Invalid token:", t.token, f"[{self.peek()}]", scope.last == t.token
210
+ # )
211
+ raise InvalidState(t.token)
212
+
213
+ last_is_space = curr_is_space
214
+ start = False
autosh/md/printer.py ADDED
@@ -0,0 +1,301 @@
1
+ import os
2
+ from typing import AsyncGenerator, Literal
3
+ import unicodedata
4
+
5
+ from autosh.md.state import Color, State
6
+ from autosh.md.stream import TextStream
7
+
8
+
9
+ class StreamedMarkdownPrinter:
10
+ def __init__(self, gen: AsyncGenerator[str, None]):
11
+ self.stream = TextStream(gen)
12
+ self.state = State()
13
+
14
+ def emit(self, s: str):
15
+ self.state.emit(s)
16
+
17
+ def peek(self):
18
+ return self.stream.peek()
19
+
20
+ async def consume(self, n: int = 1):
21
+ return await self.stream.consume(n)
22
+
23
+ async def check(self, s: str, eof: bool | None = None) -> bool:
24
+ return await self.stream.check(s, eof)
25
+
26
+ async def parse_inline(self, consume_trailing_newline: bool = True):
27
+ from autosh.md.inline_text import InlineTextPrinter
28
+
29
+ itp = InlineTextPrinter(self)
30
+ await itp.parse_inline(consume_trailing_newline)
31
+
32
+ async def parse_heading(self):
33
+ hashes = 0
34
+ while True:
35
+ c = await self.consume()
36
+ if c == "#":
37
+ hashes += 1
38
+ else:
39
+ break
40
+ match hashes:
41
+ case 1:
42
+ with self.state.style(bold=True, bg=Color.MAGENTA):
43
+ with self.state.style(dim=True):
44
+ self.emit("#" * hashes + " ")
45
+ await self.parse_inline(consume_trailing_newline=False)
46
+ case 2:
47
+ with self.state.style(bold=True, underline=True, color=Color.MAGENTA):
48
+ with self.state.style(dim=True):
49
+ self.emit("#" * hashes + " ")
50
+ await self.parse_inline(consume_trailing_newline=False)
51
+ case 3:
52
+ with self.state.style(bold=True, color=Color.MAGENTA):
53
+ with self.state.style(dim=True):
54
+ self.emit("#" * hashes + " ")
55
+ await self.parse_inline(consume_trailing_newline=False)
56
+ case 4:
57
+ with self.state.style(bold=True, italic=True, color=Color.MAGENTA):
58
+ with self.state.style(dim=True):
59
+ self.emit("#" * hashes + " ")
60
+ await self.parse_inline(consume_trailing_newline=False)
61
+ case _:
62
+ with self.state.style(bold=True):
63
+ with self.state.style(dim=True):
64
+ self.emit("#" * hashes + " ")
65
+ await self.parse_inline(consume_trailing_newline=False)
66
+ await self.consume() # consume the newline
67
+ self.emit("\n")
68
+
69
+ async def parse_paragraph(self):
70
+ while True:
71
+ await self.parse_inline()
72
+ if (
73
+ self.peek() != "\n"
74
+ and not await self.stream.non_paragraph_block_start()
75
+ ):
76
+ if self.peek() == "\n":
77
+ await self.consume()
78
+ break
79
+ else:
80
+ break
81
+
82
+ async def parse_multiline_code(self):
83
+ with self.state.style(dim=True):
84
+ self.emit("```")
85
+ await self.consume(3)
86
+ while not await self.check("\n```"):
87
+ c = await self.consume()
88
+ if c is None:
89
+ self.emit("\n")
90
+ return
91
+ self.emit(c)
92
+ self.emit("\n```\n")
93
+ await self.consume(4)
94
+
95
+ async def parse_list(self, ordered: bool):
96
+ indents = [0]
97
+ counter = [1]
98
+ # first item
99
+ if ordered:
100
+ self.emit("1. ")
101
+ await self.consume()
102
+ else:
103
+ self.emit("• ")
104
+ await self.consume()
105
+ await self.parse_inline()
106
+ while True:
107
+ indent = 0
108
+ while self.peek() in [" ", "\t", "\n"]:
109
+ if self.peek() in [" ", "\t"]:
110
+ indent += 1
111
+ if self.peek() == "\n":
112
+ indent = 0
113
+ await self.consume()
114
+ if self.peek() is None:
115
+ return
116
+ if ordered and not await self.stream.ordered_list_label():
117
+ return
118
+ if not ordered and not await self.stream.unordered_list_label():
119
+ return
120
+ if not ordered:
121
+ await self.consume()
122
+ else:
123
+ while self.peek() is not None and self.peek() != ".":
124
+ await self.consume()
125
+ await self.consume()
126
+
127
+ depth = None
128
+ for i in range(len(indents) - 1):
129
+ if indents[i] <= indent and indents[i + 1] > indent:
130
+ depth = i
131
+ break
132
+ if depth is None and indents[-1] + 2 <= indent:
133
+ # indent one more level
134
+ indents.append(indent)
135
+ depth = len(indents) - 1
136
+ counter.append(1)
137
+ elif depth is None:
138
+ # same as last level
139
+ depth = len(indents) - 1
140
+ counter[depth] += 1
141
+ else:
142
+ # dedent
143
+ indents = indents[: depth + 1]
144
+ counter = counter[: depth + 1]
145
+ counter[depth] += 1
146
+ if not ordered:
147
+ self.emit(" " * depth + "• ")
148
+ else:
149
+ self.emit(" " * depth + str(counter[depth]) + ". ")
150
+ await self.parse_inline()
151
+
152
+ async def parse_blockquote(self):
153
+ from autosh.md.inline_text import InlineTextPrinter
154
+
155
+ while True:
156
+ while self.peek() in [" ", "\t"]:
157
+ await self.consume()
158
+ if self.peek() != ">":
159
+ break
160
+ await self.consume()
161
+ with self.state.style(bold=True, dim=True):
162
+ self.emit(">")
163
+ with self.state.style(dim=True):
164
+ itp = InlineTextPrinter(self)
165
+ await itp.parse_inline_unformatted()
166
+ if self.peek() == "\n":
167
+ self.emit("\n")
168
+ await self.consume()
169
+
170
+ async def parse_table(self):
171
+ def str_size(s: str) -> int:
172
+ size = 0
173
+ for c in s:
174
+ match unicodedata.east_asian_width(c):
175
+ case "F" | "W":
176
+ size += 2
177
+ case _:
178
+ size += 1
179
+ return size
180
+
181
+ rows: list[list[str]] = []
182
+ while True:
183
+ if self.peek() != "|":
184
+ break
185
+ s = "|"
186
+ await self.consume()
187
+ while self.peek() not in ["\n", None]:
188
+ s += self.peek() or ""
189
+ await self.consume()
190
+ if self.peek() == "\n":
191
+ await self.consume()
192
+ s = s.strip("|")
193
+ rows.append([c.strip() for c in s.split("|")])
194
+ # not enough rows
195
+ if len(rows) < 2:
196
+ self.emit("| " + " | ".join(rows[0]) + " |\n")
197
+ # do some simple formatting
198
+ cols = len(rows[0])
199
+ col_widths = [0] * cols
200
+ aligns: list[Literal["left", "right", "center"]] = ["left"] * cols
201
+ for i, row in enumerate(rows):
202
+ if i == 1:
203
+ # check for alignment
204
+ for j, c in enumerate(row):
205
+ if c.startswith(":") and c.endswith(":"):
206
+ aligns[j] = "center"
207
+ elif c.startswith(":"):
208
+ aligns[j] = "left"
209
+ elif c.endswith(":"):
210
+ aligns[j] = "right"
211
+ continue
212
+ for j, c in enumerate(row):
213
+ col_widths[j] = max(col_widths[j], str_size(c))
214
+ # print top border
215
+ with self.state.style(dim=True):
216
+ for j, c in enumerate(rows[0]):
217
+ self.emit("┌" if j == 0 else "┬")
218
+ self.emit("─" * (col_widths[j] + 2))
219
+ self.emit("┐\n")
220
+ # print the table
221
+ for i, row in enumerate(rows):
222
+ for j, c in enumerate(row):
223
+ with self.state.style(dim=True):
224
+ self.emit("│" if i != 1 else ("├" if j == 0 else "┼"))
225
+ if i == 1:
226
+ text = "─" * (col_widths[j] + 2)
227
+ else:
228
+ s = str_size(c)
229
+ align = aligns[j] if i != 0 else "center"
230
+ match align:
231
+ case "left":
232
+ text = c + " " * (col_widths[j] - s)
233
+ case "right":
234
+ text = " " * (col_widths[j] - s) + c
235
+ case "center":
236
+ padding = col_widths[j] - s
237
+ text = " " * (padding // 2) + c
238
+ text += " " * (padding - padding // 2)
239
+ text = f" {text} "
240
+ with self.state.style(dim=i == 1):
241
+ self.emit(text)
242
+ with self.state.style(dim=True):
243
+ self.emit("│\n" if i != 1 else "┤\n")
244
+ # print bottom border
245
+ with self.state.style(dim=True):
246
+ for j, c in enumerate(rows[0]):
247
+ self.emit("└" if j == 0 else "┴")
248
+ self.emit("─" * (col_widths[j] + 2))
249
+ self.emit("┘\n")
250
+
251
+ async def parse_doc(self):
252
+ await self.stream.init()
253
+ start = True
254
+ while True:
255
+ # Remove leading spaces and empty lines
256
+ indent = 0
257
+ while self.peek() in [" ", "\t", "\n"]:
258
+ if self.peek() in [" ", "\t"]:
259
+ indent += 1
260
+ if self.peek() == "\n":
261
+ indent = 0
262
+ await self.consume()
263
+ if self.peek() is None:
264
+ break
265
+ if not start:
266
+ self.emit("\n")
267
+ start = False
268
+ match c := self.peek():
269
+ case None:
270
+ break
271
+ # Heading
272
+ case "#":
273
+ await self.parse_heading()
274
+ # Code
275
+ case "`" if await self.check("```"):
276
+ await self.parse_multiline_code()
277
+ # Separator
278
+ case _ if await self.check("---"):
279
+ await self.consume(3)
280
+ width = min(os.get_terminal_size().columns, 80)
281
+ with self.state.style(dim=True):
282
+ self.emit("─" * width)
283
+ # Unordered list
284
+ case _ if await self.stream.unordered_list_label():
285
+ await self.parse_list(False)
286
+ # Ordered list
287
+ case _ if await self.stream.ordered_list_label():
288
+ await self.parse_list(True)
289
+ # Blockquote
290
+ case ">":
291
+ await self.parse_blockquote()
292
+ # Table
293
+ case "|":
294
+ await self.parse_table()
295
+ # Normal paragraph
296
+ case _:
297
+ await self.parse_paragraph()
298
+ self.emit("\x1b[0m") # Reset all
299
+
300
+ def __await__(self):
301
+ return self.parse_doc().__await__()
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/session.py CHANGED
@@ -88,7 +88,7 @@ class Session:
88
88
  def _get_argv_message(self):
89
89
  args = str(CLI_OPTIONS.args)
90
90
  if not CLI_OPTIONS.script:
91
- cmd = "PROMPT"
91
+ cmd = Path(sys.argv[0]).name
92
92
  else:
93
93
  cmd = CLI_OPTIONS.script.name
94
94
  return UserMessage(
@@ -116,7 +116,21 @@ class Session:
116
116
  if CLI_OPTIONS.stdin_has_data():
117
117
  self.agent.history.add(
118
118
  UserMessage(
119
- 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.",
120
134
  role="user",
121
135
  )
122
136
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autosh
3
- Version: 0.0.2
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,,
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