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.
@@ -0,0 +1,18 @@
1
+ /target
2
+
3
+ # Python-generated files
4
+ __pycache__/
5
+ *.py[oc]
6
+ build/
7
+ dist/
8
+ wheels/
9
+ *.egg-info
10
+
11
+ # Virtual environments
12
+ .venv
13
+
14
+ # Environment variables
15
+ .env
16
+
17
+ # Other files
18
+ /_*
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021 Arnaud Blois
3
+ Copyright (c) 2024 Wenyu Zhao
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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)