autosh 0.0.0__tar.gz → 0.0.1__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.
- autosh-0.0.1/.gitignore +18 -0
- {autosh-0.0.0 → autosh-0.0.1}/LICENSE +1 -1
- autosh-0.0.1/PKG-INFO +77 -0
- autosh-0.0.1/README.md +60 -0
- autosh-0.0.1/autosh/config.py +78 -0
- autosh-0.0.1/autosh/main.py +164 -0
- autosh-0.0.1/autosh/md.py +394 -0
- autosh-0.0.1/autosh/plugins/__init__.py +87 -0
- autosh-0.0.1/autosh/plugins/calc.py +22 -0
- autosh-0.0.1/autosh/plugins/cli.py +222 -0
- autosh-0.0.1/autosh/plugins/clock.py +20 -0
- autosh-0.0.1/autosh/plugins/code.py +68 -0
- autosh-0.0.1/autosh/plugins/search.py +90 -0
- autosh-0.0.1/autosh/plugins/web.py +73 -0
- autosh-0.0.1/autosh/session.py +193 -0
- autosh-0.0.1/pyproject.toml +34 -0
- autosh-0.0.0/PKG-INFO +0 -16
- autosh-0.0.0/README.md +0 -1
- autosh-0.0.0/pyproject.toml +0 -16
- {autosh-0.0.0/src/reserved → autosh-0.0.1/autosh}/__init__.py +0 -0
autosh-0.0.1/.gitignore
ADDED
autosh-0.0.1/PKG-INFO
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: autosh
|
3
|
+
Version: 0.0.1
|
4
|
+
Summary: Add your description here
|
5
|
+
License-File: LICENSE
|
6
|
+
Requires-Python: >=3.13
|
7
|
+
Requires-Dist: agentia>=0.0.5
|
8
|
+
Requires-Dist: asyncio>=3.4.3
|
9
|
+
Requires-Dist: markdownify>=1.1.0
|
10
|
+
Requires-Dist: pydantic>=2.11.3
|
11
|
+
Requires-Dist: python-dotenv>=1.1.0
|
12
|
+
Requires-Dist: rich>=14.0.0
|
13
|
+
Requires-Dist: tavily-python>=0.5.4
|
14
|
+
Requires-Dist: typer>=0.12.5
|
15
|
+
Requires-Dist: tzlocal>=5.3.1
|
16
|
+
Description-Content-Type: text/markdown
|
17
|
+
|
18
|
+
# `autosh` - The AI-powered, noob-friendly interactive shell
|
19
|
+
|
20
|
+
# Getting Started
|
21
|
+
|
22
|
+
## Install
|
23
|
+
|
24
|
+
```bash
|
25
|
+
uv tool install autosh
|
26
|
+
```
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
As an interactive shell: `ash` (alternatively, `autosh`)
|
31
|
+
|
32
|
+
Execute a single prompt: `ash "list current directory"`
|
33
|
+
|
34
|
+
Process piped data: `cat README.md | ash "summarise"`
|
35
|
+
|
36
|
+
## Scripting
|
37
|
+
|
38
|
+
Write AI-powered shell scripts in Markdown using natural language!
|
39
|
+
|
40
|
+
Example script ([simple.a.md](examples/simple.a.md)):
|
41
|
+
|
42
|
+
```markdown
|
43
|
+
#!/usr/bin/env ash
|
44
|
+
|
45
|
+
# This is a simple file manipulation script
|
46
|
+
|
47
|
+
First, please display a welcome message:)
|
48
|
+
|
49
|
+
Write "Hello, world" to _test.log
|
50
|
+
```
|
51
|
+
|
52
|
+
* Run the script: `ash simple.a.md` or `chmod +x simple.a.md && simple.a.md`
|
53
|
+
* Auto generate help messages:
|
54
|
+
|
55
|
+
```console
|
56
|
+
$ ash simple.a.md -h
|
57
|
+
|
58
|
+
Usage: simple.a.md [OPTIONS]
|
59
|
+
|
60
|
+
This is a simple file manipulation script that writes "Hello, world" to a log file named _x.log.
|
61
|
+
|
62
|
+
Options:
|
63
|
+
|
64
|
+
• -h, --help Show this message and exit.
|
65
|
+
```
|
66
|
+
|
67
|
+
## Plugins
|
68
|
+
|
69
|
+
`autosh` is equipped with several plugins to expand its potential:
|
70
|
+
|
71
|
+
* `ash "Create a directory "my-news", list the latest news, for each news, put the summary in a separate markdown file in this directory"`
|
72
|
+
|
73
|
+
# TODO
|
74
|
+
|
75
|
+
- [ ] Image generation
|
76
|
+
- [ ] Image input
|
77
|
+
- [ ] RAG for non-text files
|
autosh-0.0.1/README.md
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# `autosh` - The AI-powered, noob-friendly interactive shell
|
2
|
+
|
3
|
+
# Getting Started
|
4
|
+
|
5
|
+
## Install
|
6
|
+
|
7
|
+
```bash
|
8
|
+
uv tool install autosh
|
9
|
+
```
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
As an interactive shell: `ash` (alternatively, `autosh`)
|
14
|
+
|
15
|
+
Execute a single prompt: `ash "list current directory"`
|
16
|
+
|
17
|
+
Process piped data: `cat README.md | ash "summarise"`
|
18
|
+
|
19
|
+
## Scripting
|
20
|
+
|
21
|
+
Write AI-powered shell scripts in Markdown using natural language!
|
22
|
+
|
23
|
+
Example script ([simple.a.md](examples/simple.a.md)):
|
24
|
+
|
25
|
+
```markdown
|
26
|
+
#!/usr/bin/env ash
|
27
|
+
|
28
|
+
# This is a simple file manipulation script
|
29
|
+
|
30
|
+
First, please display a welcome message:)
|
31
|
+
|
32
|
+
Write "Hello, world" to _test.log
|
33
|
+
```
|
34
|
+
|
35
|
+
* Run the script: `ash simple.a.md` or `chmod +x simple.a.md && simple.a.md`
|
36
|
+
* Auto generate help messages:
|
37
|
+
|
38
|
+
```console
|
39
|
+
$ ash simple.a.md -h
|
40
|
+
|
41
|
+
Usage: simple.a.md [OPTIONS]
|
42
|
+
|
43
|
+
This is a simple file manipulation script that writes "Hello, world" to a log file named _x.log.
|
44
|
+
|
45
|
+
Options:
|
46
|
+
|
47
|
+
• -h, --help Show this message and exit.
|
48
|
+
```
|
49
|
+
|
50
|
+
## Plugins
|
51
|
+
|
52
|
+
`autosh` is equipped with several plugins to expand its potential:
|
53
|
+
|
54
|
+
* `ash "Create a directory "my-news", list the latest news, for each news, put the summary in a separate markdown file in this directory"`
|
55
|
+
|
56
|
+
# TODO
|
57
|
+
|
58
|
+
- [ ] Image generation
|
59
|
+
- [ ] Image input
|
60
|
+
- [ ] RAG for non-text files
|
@@ -0,0 +1,78 @@
|
|
1
|
+
import sys
|
2
|
+
from pydantic import BaseModel, Field
|
3
|
+
from pathlib import Path
|
4
|
+
import tomllib
|
5
|
+
|
6
|
+
USER_CONFIG_PATH = Path.home() / ".config" / "autosh" / "config.toml"
|
7
|
+
|
8
|
+
|
9
|
+
class EmptyConfig(BaseModel): ...
|
10
|
+
|
11
|
+
|
12
|
+
class SearchConfig(BaseModel):
|
13
|
+
tavily_api_key: str = Field(..., description="Tavily API key.")
|
14
|
+
|
15
|
+
|
16
|
+
class WebConfig(BaseModel):
|
17
|
+
tavily_api_key: str = Field(..., description="Tavily API key.")
|
18
|
+
|
19
|
+
|
20
|
+
class Plugins(BaseModel):
|
21
|
+
calc: EmptyConfig | None = None
|
22
|
+
cli: EmptyConfig | None = None
|
23
|
+
clock: EmptyConfig | None = None
|
24
|
+
code: EmptyConfig | None = None
|
25
|
+
search: SearchConfig | None = None
|
26
|
+
web: WebConfig | None = None
|
27
|
+
|
28
|
+
|
29
|
+
class Config(BaseModel):
|
30
|
+
model: str = Field(default="openai/gpt-4.1", description="The LLM model to use")
|
31
|
+
think_model: str = Field(
|
32
|
+
default="openai/o4-mini-high",
|
33
|
+
description="The LLM model to use for reasoning before executing commands",
|
34
|
+
)
|
35
|
+
api_key: str | None = Field(default=None, description="OpenRouter API key.")
|
36
|
+
|
37
|
+
plugins: Plugins = Field(
|
38
|
+
default_factory=Plugins,
|
39
|
+
description="Plugin configuration. Set to null to disable the plugin.",
|
40
|
+
)
|
41
|
+
|
42
|
+
@staticmethod
|
43
|
+
def load() -> "Config":
|
44
|
+
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})
|
49
|
+
else:
|
50
|
+
config = Config()
|
51
|
+
return config
|
52
|
+
|
53
|
+
|
54
|
+
CONFIG = Config.load()
|
55
|
+
|
56
|
+
|
57
|
+
class CLIOptions(BaseModel):
|
58
|
+
yes: bool = False
|
59
|
+
quiet: bool = False
|
60
|
+
think: bool = False
|
61
|
+
|
62
|
+
prompt: str | None = None
|
63
|
+
"""The prompt to execute"""
|
64
|
+
|
65
|
+
script: Path | None = None
|
66
|
+
"""The scripe providing the prompt"""
|
67
|
+
|
68
|
+
stdin_is_script: bool = False
|
69
|
+
"""STDIN is a script, not a piped input."""
|
70
|
+
|
71
|
+
args: list[str] = Field(default_factory=list, description="Command line arguments")
|
72
|
+
|
73
|
+
def stdin_has_data(self) -> bool:
|
74
|
+
"""Check if stdin has data."""
|
75
|
+
return not sys.stdin.isatty() and not CLI_OPTIONS.stdin_is_script
|
76
|
+
|
77
|
+
|
78
|
+
CLI_OPTIONS = CLIOptions()
|
@@ -0,0 +1,164 @@
|
|
1
|
+
import os
|
2
|
+
from pathlib import Path
|
3
|
+
import rich
|
4
|
+
import typer
|
5
|
+
import asyncio
|
6
|
+
import dotenv
|
7
|
+
from rich.columns import Columns
|
8
|
+
from rich.panel import Panel
|
9
|
+
import argparse
|
10
|
+
|
11
|
+
from autosh.config import CLI_OPTIONS, CONFIG
|
12
|
+
from .session import Session
|
13
|
+
import sys
|
14
|
+
|
15
|
+
|
16
|
+
app = typer.Typer(
|
17
|
+
no_args_is_help=False,
|
18
|
+
add_completion=False,
|
19
|
+
context_settings=dict(help_option_names=["-h", "--help"]),
|
20
|
+
pretty_exceptions_short=True,
|
21
|
+
pretty_exceptions_show_locals=False,
|
22
|
+
help="Autosh is a command line tool that helps you automate your tasks using LLMs.",
|
23
|
+
)
|
24
|
+
|
25
|
+
|
26
|
+
async def start_session(prompt: str | None, args: list[str]):
|
27
|
+
CLI_OPTIONS.args = args
|
28
|
+
session = Session()
|
29
|
+
await session.init()
|
30
|
+
piped_stdin = not sys.stdin.isatty()
|
31
|
+
if piped_stdin and not CLI_OPTIONS.yes:
|
32
|
+
rich.print(
|
33
|
+
"[bold red]Error:[/bold red] [red]--yes is required when using piped stdin.[/red]"
|
34
|
+
)
|
35
|
+
sys.exit(1)
|
36
|
+
if prompt:
|
37
|
+
# No piped stdin, just run the prompt
|
38
|
+
if Path(prompt).is_file():
|
39
|
+
# Prompt is a file, read it and execute it
|
40
|
+
await session.exec_script(Path(prompt))
|
41
|
+
else:
|
42
|
+
# Prompt is a string, execute it directly
|
43
|
+
await session.exec_prompt(prompt)
|
44
|
+
elif not prompt and not sys.stdin.isatty():
|
45
|
+
# Piped stdin without prompt, treat piped stdin as a prompt.
|
46
|
+
await session.exec_from_stdin()
|
47
|
+
else:
|
48
|
+
await session.run_repl()
|
49
|
+
|
50
|
+
|
51
|
+
def print_help():
|
52
|
+
cmd = Path(sys.argv[0]).name
|
53
|
+
rich.print(
|
54
|
+
f"\n [bold yellow]Usage:[/bold yellow] [bold]{cmd} [OPTIONS] [--] [PROMPT_OR_FILE] [ARGS]...[/bold]\n"
|
55
|
+
)
|
56
|
+
|
57
|
+
args = [
|
58
|
+
["prompt_or_file", "[PROMPT_OR_FILE]", "The prompt or file to execute."],
|
59
|
+
["args", "[ARGS]...", "The arguments to pass to the script."],
|
60
|
+
]
|
61
|
+
options = [
|
62
|
+
["--yes", "-y", "Auto confirm all prompts."],
|
63
|
+
["--quiet", "-q", "Suppress all output."],
|
64
|
+
[
|
65
|
+
"--model",
|
66
|
+
"-m",
|
67
|
+
f"The LLM model to use. [dim]Default: {CONFIG.model} ({CONFIG.think_model} for reasoning).[/dim]",
|
68
|
+
],
|
69
|
+
["--think", "", "Use the reasoning models to think more before operating."],
|
70
|
+
["--help", "-h", "Show this message and exit."],
|
71
|
+
]
|
72
|
+
|
73
|
+
rich.print(
|
74
|
+
Panel.fit(
|
75
|
+
Columns(
|
76
|
+
[
|
77
|
+
"\n".join([a[0] for a in args]),
|
78
|
+
"\n".join([f"[bold yellow]\\{a[1]}[/bold yellow]" for a in args]),
|
79
|
+
"\n".join([" " + a[2] for a in args]),
|
80
|
+
],
|
81
|
+
padding=(0, 3),
|
82
|
+
),
|
83
|
+
title="[dim]Arguments[/dim]",
|
84
|
+
title_align="left",
|
85
|
+
padding=(0, 3),
|
86
|
+
)
|
87
|
+
)
|
88
|
+
|
89
|
+
rich.print(
|
90
|
+
Panel.fit(
|
91
|
+
Columns(
|
92
|
+
[
|
93
|
+
"\n".join([f"[bold blue]{o[0]}[/bold blue]" for o in options]),
|
94
|
+
"\n".join([f"[bold green]{o[1]}[/bold green]" for o in options]),
|
95
|
+
"\n".join([" " + o[2] for o in options]),
|
96
|
+
],
|
97
|
+
padding=(0, 2),
|
98
|
+
),
|
99
|
+
title="[dim]Options[/dim]",
|
100
|
+
title_align="left",
|
101
|
+
padding=(0, 3),
|
102
|
+
)
|
103
|
+
)
|
104
|
+
|
105
|
+
|
106
|
+
def parse_args() -> tuple[str | None, list[str]]:
|
107
|
+
p = argparse.ArgumentParser(add_help=False, exit_on_error=False)
|
108
|
+
|
109
|
+
p.add_argument("--help", "-h", action="store_true")
|
110
|
+
p.add_argument("--yes", "-y", action="store_true")
|
111
|
+
p.add_argument("--quiet", "-q", action="store_true")
|
112
|
+
p.add_argument("--think", action="store_true")
|
113
|
+
p.add_argument("--model", "-m", type=str, default=None)
|
114
|
+
p.add_argument("PROMPT_OR_FILE", nargs="?", default=None)
|
115
|
+
p.add_argument("ARGS", nargs=argparse.REMAINDER)
|
116
|
+
|
117
|
+
try:
|
118
|
+
args = p.parse_args()
|
119
|
+
except argparse.ArgumentError as e:
|
120
|
+
rich.print(f"[bold red]Error:[/bold red] {str(e)}")
|
121
|
+
print_help()
|
122
|
+
sys.exit(1)
|
123
|
+
|
124
|
+
if args.help:
|
125
|
+
print_help()
|
126
|
+
sys.exit(0)
|
127
|
+
|
128
|
+
CLI_OPTIONS.yes = args.yes
|
129
|
+
CLI_OPTIONS.quiet = args.quiet
|
130
|
+
|
131
|
+
if args.model:
|
132
|
+
if args.think:
|
133
|
+
CONFIG.think_model = args.model
|
134
|
+
else:
|
135
|
+
CONFIG.model = args.model
|
136
|
+
|
137
|
+
if args.think:
|
138
|
+
CLI_OPTIONS.think = True
|
139
|
+
|
140
|
+
prompt = args.PROMPT_OR_FILE.strip() if args.PROMPT_OR_FILE else None
|
141
|
+
|
142
|
+
if prompt == "":
|
143
|
+
prompt = None
|
144
|
+
|
145
|
+
return prompt, (args.ARGS or [])
|
146
|
+
|
147
|
+
|
148
|
+
def main():
|
149
|
+
# dotenv.load_dotenv()
|
150
|
+
prompt, args = parse_args()
|
151
|
+
|
152
|
+
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)
|
160
|
+
try:
|
161
|
+
asyncio.run(start_session(prompt, args))
|
162
|
+
except (KeyboardInterrupt, EOFError):
|
163
|
+
rich.print("\n[red]Aborted.[/red]")
|
164
|
+
sys.exit(1)
|