ape-linux 0.3.5__tar.gz → 0.4.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.
- {ape_linux-0.3.5 → ape_linux-0.4.1}/.github/workflows/tests.yaml +7 -3
- ape_linux-0.4.1/.python-version +1 -0
- ape_linux-0.4.1/CLAUDE.md +104 -0
- {ape_linux-0.3.5 → ape_linux-0.4.1}/LICENSE +1 -1
- ape_linux-0.4.1/PKG-INFO +136 -0
- ape_linux-0.4.1/README.md +117 -0
- ape_linux-0.4.1/ape_linux.py +240 -0
- ape_linux-0.4.1/justfile +11 -0
- {ape_linux-0.3.5 → ape_linux-0.4.1}/pyproject.toml +7 -10
- ape_linux-0.4.1/tests/test_app.py +150 -0
- ape_linux-0.4.1/uv.lock +1449 -0
- ape_linux-0.3.5/.python-version +0 -1
- ape_linux-0.3.5/PKG-INFO +0 -118
- ape_linux-0.3.5/README.md +0 -97
- ape_linux-0.3.5/ape_linux.py +0 -140
- ape_linux-0.3.5/tests/test_app.py +0 -61
- ape_linux-0.3.5/uv.lock +0 -572
- {ape_linux-0.3.5 → ape_linux-0.4.1}/.github/workflows/publish.yaml +0 -0
- {ape_linux-0.3.5 → ape_linux-0.4.1}/.gitignore +0 -0
|
@@ -20,10 +20,12 @@ jobs:
|
|
|
20
20
|
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
21
21
|
- name: Set up Python ${{ matrix.python-version }}
|
|
22
22
|
run: uv python install ${{ matrix.python-version }}
|
|
23
|
+
- name: Set up just
|
|
24
|
+
uses: extractions/setup-just@v3
|
|
23
25
|
- name: Run type check
|
|
24
|
-
run:
|
|
26
|
+
run: just type-check
|
|
25
27
|
- name: Run tests
|
|
26
|
-
run:
|
|
28
|
+
run: just test
|
|
27
29
|
|
|
28
30
|
lint:
|
|
29
31
|
name: Lint
|
|
@@ -35,5 +37,7 @@ jobs:
|
|
|
35
37
|
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
36
38
|
- name: Set up Python 3.13
|
|
37
39
|
run: uv python install 3.13
|
|
40
|
+
- name: Set up just
|
|
41
|
+
uses: extractions/setup-just@v3
|
|
38
42
|
- name: Ruff
|
|
39
|
-
run:
|
|
43
|
+
run: just lint
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
> **Keep this file current.** Whenever you make a change that affects anything
|
|
6
|
+
> described here — behavior, commands, architecture, conventions, defaults, file
|
|
7
|
+
> layout — update the relevant section of CLAUDE.md (and `README.md` where it
|
|
8
|
+
> overlaps) in the same change, so the docs never drift from the code.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Ape (`ape-linux` on PyPI) is a CLI that turns a natural-language description of a
|
|
13
|
+
Linux task into a shell command using an LLM. The entire implementation lives in a
|
|
14
|
+
single module, `ape_linux.py`, exposing two console scripts: `ape` (→ `main()`) and
|
|
15
|
+
`ape-system-info` (→ `system_info()`). It has no CLI framework — argument handling is
|
|
16
|
+
plain `sys.argv`.
|
|
17
|
+
|
|
18
|
+
**Single-file rule:** all of Ape's implementation must stay in `ape_linux.py` — do
|
|
19
|
+
not split it into additional modules/packages. New behavior is added as functions in
|
|
20
|
+
this one file.
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
This project uses [`uv`](https://docs.astral.sh/uv/) for everything, with
|
|
25
|
+
[`just`](https://github.com/casey/just) wrapping the CI-relevant tasks.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
just lint # lint (uv run ruff check .)
|
|
29
|
+
just type-check # type check with ty (uv run ty check)
|
|
30
|
+
just test # run all tests (uv run pytest)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The `justfile` recipes are the single source of truth for lint, type-check, and test:
|
|
34
|
+
both local dev and CI (`.github/workflows/tests.yaml`) invoke them, so the two can't
|
|
35
|
+
drift. CI installs `just` via the `extractions/setup-just` action. Other useful
|
|
36
|
+
commands:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv sync # install deps + dev group into .venv
|
|
40
|
+
uv run pytest tests/test_app.py::test_app_for_version # run a single test
|
|
41
|
+
uv run ruff format . # format
|
|
42
|
+
uv run ape "list all files" # run the CLI locally
|
|
43
|
+
uv build # build the wheel/sdist
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Architecture
|
|
47
|
+
|
|
48
|
+
The flow in `ape_linux.py` is intentionally minimal:
|
|
49
|
+
|
|
50
|
+
1. `main()` is the `ape` entry point. It builds the query by joining `sys.argv[1:]`
|
|
51
|
+
with spaces, so both `ape "list files"` and `ape list files` work. With no
|
|
52
|
+
arguments it prints the `HELP` text and exits with code 1. There are no flags —
|
|
53
|
+
model selection, execution, version, and system-info are all gone. A separate
|
|
54
|
+
`system_info()` function backs the `ape-system-info` console script; it just prints
|
|
55
|
+
`detect_system_context()`.
|
|
56
|
+
2. A hard-coded `system_prompt` (with few-shot examples) constrains the model to emit
|
|
57
|
+
**only** a runnable command — no Markdown fences — or `echo "Please try again."`
|
|
58
|
+
for anything off-topic. This prompt is the core product behavior; changes to it
|
|
59
|
+
directly change what the tool outputs. `main()` then appends a system-context block
|
|
60
|
+
(see below) so suggestions match the current machine.
|
|
61
|
+
2a. `detect_system_context()` returns a best-effort, newline-separated `Key: value`
|
|
62
|
+
block describing the current machine (OS family + macOS/distro version, GNU-vs-BSD
|
|
63
|
+
userland, CPU arch, `$SHELL`, root-or-not, available package managers, and a probed
|
|
64
|
+
list of common tools). Two invariants: it **never raises** (every probe is guarded,
|
|
65
|
+
so an unavailable API or missing file just omits that field instead of crashing at
|
|
66
|
+
startup), and it **never includes identifying info** (no username, hostname, working
|
|
67
|
+
directory, or home path). Everything is stdlib (`platform`, `os`, `shutil.which`) and
|
|
68
|
+
stat-based — no subprocesses — to keep startup fast. Notable guards:
|
|
69
|
+
`platform.freedesktop_os_release()` raises `OSError` on macOS/minimal containers, and
|
|
70
|
+
`os.geteuid()` is absent on non-Unix platforms (`hasattr` check).
|
|
71
|
+
3. `call_llm()` wraps `pydantic_ai.Agent`, which is the provider abstraction. Models
|
|
72
|
+
are passed through verbatim in `provider:name` form (e.g. `anthropic:claude-sonnet-4-5`),
|
|
73
|
+
so Ape supports any provider Pydantic AI supports without provider-specific code.
|
|
74
|
+
The model is resolved solely from the `APE_MODEL` env var → the `DEFAULT_MODEL`
|
|
75
|
+
constant (`openai-chat:gpt-4.1`); there is no CLI override. Credentials come from
|
|
76
|
+
each provider's standard env var (e.g. `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`).
|
|
77
|
+
4. Errors are flattened to one-line stderr messages, raising `SystemExit(1)` —
|
|
78
|
+
`ModelHTTPError` reports status/message; any other exception (bad credentials,
|
|
79
|
+
unknown provider) prints `str(error)`. There is no CLI framework swallowing
|
|
80
|
+
tracebacks, so exceptions are caught explicitly.
|
|
81
|
+
|
|
82
|
+
## Testing
|
|
83
|
+
|
|
84
|
+
`tests/test_app.py` drives `main()` directly via a `run()` helper that monkeypatches
|
|
85
|
+
`sys.argv` and returns the `SystemExit` code (0 on the success path, since `main()`
|
|
86
|
+
doesn't exit then), asserting on captured stdout/stderr with `capsys`. It monkeypatches
|
|
87
|
+
`ape_linux.call_llm` so no real network/LLM calls happen. The `mockenv` fixture sets a
|
|
88
|
+
dummy `OPENAI_API_KEY`.
|
|
89
|
+
|
|
90
|
+
`detect_system_context()` is covered by tests that assert it returns a string without
|
|
91
|
+
crashing, reports the OS, and — importantly — excludes the current username, hostname,
|
|
92
|
+
working directory, and home path. These run against the real host (no mocking), so keep
|
|
93
|
+
them platform-agnostic; the username check skips the `root` collision with the privilege
|
|
94
|
+
line.
|
|
95
|
+
|
|
96
|
+
## Conventions
|
|
97
|
+
|
|
98
|
+
- Ruff targets `py314`; lint rules are `F, E, W, I001` (pyflakes, pycodestyle, import
|
|
99
|
+
sorting). `ape_linux` is the known first-party package for import ordering.
|
|
100
|
+
- The published package requires Python >=3.10; CI matrixes 3.10–3.13, while local dev
|
|
101
|
+
uses 3.14 (`.python-version`).
|
|
102
|
+
- Version is read at runtime from package metadata (`importlib.metadata.version`), so
|
|
103
|
+
the single source of truth is `pyproject.toml`'s `version`. Publishing is tag-driven
|
|
104
|
+
(`.github/workflows/publish.yaml`).
|
ape_linux-0.4.1/PKG-INFO
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ape-linux
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: AI for Linux commands
|
|
5
|
+
Project-URL: Homepage, https://github.com/sbalian/ape
|
|
6
|
+
Project-URL: Repository, https://github.com/sbalian/ape
|
|
7
|
+
Author-email: Seto Balian <seto.balian@gmail.com>
|
|
8
|
+
Maintainer-email: Seto Balian <seto.balian@gmail.com>
|
|
9
|
+
License: MIT License
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,anthropic,linux,llm,openai,pydantic-ai
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Topic :: Software Development
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: pydantic-ai-slim[anthropic,google,groq,mistral,openai]>=1.104.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Ape
|
|
21
|
+
|
|
22
|
+
Ape is an AI for Linux commands.
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
ape "Find all the important PDF files in user/projects. An important PDF file has 'attention' in its name. Write the results to important_files.txt and then move it to Documents."
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Output:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
find ~/user/projects -type f -name "*attention*.pdf" > important_files.txt && mv important_files.txt ~/Documents/
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Ape works with any provider supported by [Pydantic AI](https://ai.pydantic.dev/models/)
|
|
35
|
+
— OpenAI, Anthropic, Google, Groq, Mistral and more.
|
|
36
|
+
|
|
37
|
+
To install ([`uv`](https://docs.astral.sh/uv/getting-started/installation/) recommended):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv tool install ape-linux
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Next, set the API key for your provider using its standard environment variable.
|
|
44
|
+
For example:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
export OPENAI_API_KEY=key # for OpenAI models
|
|
48
|
+
export ANTHROPIC_API_KEY=key # for Anthropic models
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
To run:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
ape "Create a symbolic link called win pointing to /mnt/c/Users/jdoe"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Output:
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
ln -s /mnt/c/Users/jdoe win
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Another example:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
ape "Delete all the .venv directories under projects/"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Output:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
find projects/ -type d -name ".venv" -exec rm -rf {} +
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
If you try to ask something unrelated to Linux commands:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
ape "Tell me about monkeys"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
you should get:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
echo "Please try again."
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
You can change the model with the `APE_MODEL` environment variable. Models are
|
|
88
|
+
specified in `provider:name` form.
|
|
89
|
+
See [here](https://ai.pydantic.dev/models/) for the supported providers and models.
|
|
90
|
+
For example:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
export APE_MODEL=anthropic:claude-sonnet-4-5
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
If `APE_MODEL` is unset, the default `openai-chat:gpt-4.1` is used.
|
|
97
|
+
|
|
98
|
+
## System-aware suggestions
|
|
99
|
+
|
|
100
|
+
Ape automatically detects a few facts about your machine and adds them to the prompt so
|
|
101
|
+
the suggested command is correct for *your* environment — for example BSD (macOS) vs GNU
|
|
102
|
+
(Linux) flags, the right package manager (`brew`, `apt`, `dnf`, `pacman`, ...), and tools
|
|
103
|
+
that are actually installed. It looks at:
|
|
104
|
+
|
|
105
|
+
- operating system and version (macOS version or Linux distribution),
|
|
106
|
+
- whether the userland is BSD or GNU,
|
|
107
|
+
- CPU architecture (e.g. `arm64` vs `x86_64`),
|
|
108
|
+
- your shell (`$SHELL`),
|
|
109
|
+
- whether you are root,
|
|
110
|
+
- available package manager(s) and common tools (`rg`, `fd`, `jq`, `docker`, ...).
|
|
111
|
+
|
|
112
|
+
This is all gathered locally with the Python standard library and is best-effort: if
|
|
113
|
+
anything can't be determined it is simply left out. **No identifying information is
|
|
114
|
+
collected or sent** — never your username, hostname, working directory, or home path.
|
|
115
|
+
|
|
116
|
+
To see exactly what Ape detects and sends (without calling the model), run
|
|
117
|
+
`ape-system-info`:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
ape-system-info
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Output (example):
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
Operating system: Darwin
|
|
127
|
+
macOS version: 26.5
|
|
128
|
+
Userland: BSD (macOS) — prefer BSD-compatible flags
|
|
129
|
+
Architecture: arm64
|
|
130
|
+
Shell: /bin/zsh
|
|
131
|
+
Privileges: non-root (use sudo for privileged actions)
|
|
132
|
+
Package manager(s): brew
|
|
133
|
+
Available tools: rg, fd, jq, git, curl, docker, tar, rsync, sed, awk
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
See also: [Gorilla](https://github.com/gorilla-llm)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Ape
|
|
2
|
+
|
|
3
|
+
Ape is an AI for Linux commands.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
ape "Find all the important PDF files in user/projects. An important PDF file has 'attention' in its name. Write the results to important_files.txt and then move it to Documents."
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Output:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
find ~/user/projects -type f -name "*attention*.pdf" > important_files.txt && mv important_files.txt ~/Documents/
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Ape works with any provider supported by [Pydantic AI](https://ai.pydantic.dev/models/)
|
|
16
|
+
— OpenAI, Anthropic, Google, Groq, Mistral and more.
|
|
17
|
+
|
|
18
|
+
To install ([`uv`](https://docs.astral.sh/uv/getting-started/installation/) recommended):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv tool install ape-linux
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Next, set the API key for your provider using its standard environment variable.
|
|
25
|
+
For example:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
export OPENAI_API_KEY=key # for OpenAI models
|
|
29
|
+
export ANTHROPIC_API_KEY=key # for Anthropic models
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
To run:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
ape "Create a symbolic link called win pointing to /mnt/c/Users/jdoe"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Output:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
ln -s /mnt/c/Users/jdoe win
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Another example:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
ape "Delete all the .venv directories under projects/"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Output:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
find projects/ -type d -name ".venv" -exec rm -rf {} +
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If you try to ask something unrelated to Linux commands:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ape "Tell me about monkeys"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
you should get:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
echo "Please try again."
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
You can change the model with the `APE_MODEL` environment variable. Models are
|
|
69
|
+
specified in `provider:name` form.
|
|
70
|
+
See [here](https://ai.pydantic.dev/models/) for the supported providers and models.
|
|
71
|
+
For example:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
export APE_MODEL=anthropic:claude-sonnet-4-5
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If `APE_MODEL` is unset, the default `openai-chat:gpt-4.1` is used.
|
|
78
|
+
|
|
79
|
+
## System-aware suggestions
|
|
80
|
+
|
|
81
|
+
Ape automatically detects a few facts about your machine and adds them to the prompt so
|
|
82
|
+
the suggested command is correct for *your* environment — for example BSD (macOS) vs GNU
|
|
83
|
+
(Linux) flags, the right package manager (`brew`, `apt`, `dnf`, `pacman`, ...), and tools
|
|
84
|
+
that are actually installed. It looks at:
|
|
85
|
+
|
|
86
|
+
- operating system and version (macOS version or Linux distribution),
|
|
87
|
+
- whether the userland is BSD or GNU,
|
|
88
|
+
- CPU architecture (e.g. `arm64` vs `x86_64`),
|
|
89
|
+
- your shell (`$SHELL`),
|
|
90
|
+
- whether you are root,
|
|
91
|
+
- available package manager(s) and common tools (`rg`, `fd`, `jq`, `docker`, ...).
|
|
92
|
+
|
|
93
|
+
This is all gathered locally with the Python standard library and is best-effort: if
|
|
94
|
+
anything can't be determined it is simply left out. **No identifying information is
|
|
95
|
+
collected or sent** — never your username, hostname, working directory, or home path.
|
|
96
|
+
|
|
97
|
+
To see exactly what Ape detects and sends (without calling the model), run
|
|
98
|
+
`ape-system-info`:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
ape-system-info
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Output (example):
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
Operating system: Darwin
|
|
108
|
+
macOS version: 26.5
|
|
109
|
+
Userland: BSD (macOS) — prefer BSD-compatible flags
|
|
110
|
+
Architecture: arm64
|
|
111
|
+
Shell: /bin/zsh
|
|
112
|
+
Privileges: non-root (use sudo for privileged actions)
|
|
113
|
+
Package manager(s): brew
|
|
114
|
+
Available tools: rg, fd, jq, git, curl, docker, tar, rsync, sed, awk
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
See also: [Gorilla](https://github.com/gorilla-llm)
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""AI for Linux commands."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from pydantic_ai import Agent
|
|
9
|
+
from pydantic_ai.exceptions import ModelHTTPError
|
|
10
|
+
|
|
11
|
+
DEFAULT_MODEL = "openai-chat:gpt-4.1"
|
|
12
|
+
|
|
13
|
+
HELP = """\
|
|
14
|
+
ape — AI for Linux commands.
|
|
15
|
+
|
|
16
|
+
Usage: ape QUERY
|
|
17
|
+
|
|
18
|
+
Describe a Linux task in QUERY and ape prints a shell command for it.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
ape "Create a symbolic link named 'win' pointing to /mnt/c/Users/jdoe"
|
|
22
|
+
ln -s /mnt/c/Users/jdoe win
|
|
23
|
+
|
|
24
|
+
The model is read from the APE_MODEL environment variable in provider:name form
|
|
25
|
+
(e.g. anthropic:claude-sonnet-4-5), falling back to {default}. See
|
|
26
|
+
https://ai.pydantic.dev/models/. Credentials come from each provider's standard
|
|
27
|
+
environment variable (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY).
|
|
28
|
+
|
|
29
|
+
Run `ape-system-info` to print the detected system context sent to the model.\
|
|
30
|
+
""".format(default=DEFAULT_MODEL)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def call_llm(model: str, system_prompt: str, user_prompt: str) -> str | None:
|
|
34
|
+
agent = Agent(
|
|
35
|
+
model,
|
|
36
|
+
system_prompt=system_prompt,
|
|
37
|
+
model_settings={"temperature": 0.2},
|
|
38
|
+
)
|
|
39
|
+
return agent.run_sync(user_prompt).output
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def detect_system_context() -> str:
|
|
43
|
+
"""Best-effort description of the current system for the LLM.
|
|
44
|
+
|
|
45
|
+
The goal is better, more correct command suggestions for *this* machine
|
|
46
|
+
(e.g. BSD vs GNU flags, the right package manager, tools that exist here).
|
|
47
|
+
|
|
48
|
+
Two hard rules:
|
|
49
|
+
|
|
50
|
+
1. **Never crash.** This runs on every invocation, so every probe is
|
|
51
|
+
guarded; a failure or a platform that lacks a given API just omits
|
|
52
|
+
that field rather than raising. The worst case is a less-informed
|
|
53
|
+
prompt, never a broken tool at startup.
|
|
54
|
+
2. **Nothing identifying.** No username, hostname, working directory, or
|
|
55
|
+
home path — only facts about the OS and installed tooling.
|
|
56
|
+
|
|
57
|
+
Returns a newline-separated block of ``Key: value`` lines (possibly
|
|
58
|
+
empty), suitable for appending to the system prompt.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
lines: list[str] = []
|
|
62
|
+
|
|
63
|
+
# OS family. platform.system() always returns a string (possibly empty)
|
|
64
|
+
# and never raises.
|
|
65
|
+
system = platform.system()
|
|
66
|
+
if system:
|
|
67
|
+
lines.append(f"Operating system: {system}")
|
|
68
|
+
|
|
69
|
+
if system == "Darwin":
|
|
70
|
+
# mac_ver() returns empty values off-Mac and never raises, but guard
|
|
71
|
+
# defensively anyway.
|
|
72
|
+
try:
|
|
73
|
+
mac_version = platform.mac_ver()[0]
|
|
74
|
+
except Exception:
|
|
75
|
+
mac_version = ""
|
|
76
|
+
if mac_version:
|
|
77
|
+
lines.append(f"macOS version: {mac_version}")
|
|
78
|
+
lines.append("Userland: BSD (macOS) — prefer BSD-compatible flags")
|
|
79
|
+
# GNU coreutils are often installed via Homebrew as g-prefixed tools.
|
|
80
|
+
if shutil.which("gls"):
|
|
81
|
+
lines.append("GNU coreutils also available (gls, gsed, gawk, ...)")
|
|
82
|
+
elif system == "Linux":
|
|
83
|
+
lines.append("Userland: GNU coreutils")
|
|
84
|
+
# freedesktop_os_release() raises OSError when /etc/os-release is
|
|
85
|
+
# absent (e.g. macOS, minimal containers), so it must be guarded.
|
|
86
|
+
try:
|
|
87
|
+
os_release = platform.freedesktop_os_release()
|
|
88
|
+
except Exception:
|
|
89
|
+
os_release = {}
|
|
90
|
+
distribution = os_release.get("PRETTY_NAME") or os_release.get("NAME")
|
|
91
|
+
if distribution:
|
|
92
|
+
lines.append(f"Distribution: {distribution}")
|
|
93
|
+
|
|
94
|
+
# CPU architecture (e.g. arm64 vs x86_64) — affects Homebrew prefixes and
|
|
95
|
+
# binary/platform names. Never raises; may be empty.
|
|
96
|
+
machine = platform.machine()
|
|
97
|
+
if machine:
|
|
98
|
+
lines.append(f"Architecture: {machine}")
|
|
99
|
+
|
|
100
|
+
# Interactive shell, which affects available syntax. May be unset.
|
|
101
|
+
shell = os.environ.get("SHELL")
|
|
102
|
+
if shell:
|
|
103
|
+
lines.append(f"Shell: {shell}")
|
|
104
|
+
|
|
105
|
+
# Privilege level. os.geteuid() does not exist on non-Unix platforms, so
|
|
106
|
+
# probe for it before calling.
|
|
107
|
+
if hasattr(os, "geteuid"):
|
|
108
|
+
if os.geteuid() == 0:
|
|
109
|
+
lines.append("Privileges: root (sudo not required)")
|
|
110
|
+
else:
|
|
111
|
+
lines.append("Privileges: non-root (use sudo for privileged actions)")
|
|
112
|
+
|
|
113
|
+
# Package manager(s). shutil.which() returns None when not found and never
|
|
114
|
+
# raises.
|
|
115
|
+
package_managers = [
|
|
116
|
+
manager
|
|
117
|
+
for manager in ("apt", "dnf", "yum", "pacman", "apk", "zypper", "brew")
|
|
118
|
+
if shutil.which(manager)
|
|
119
|
+
]
|
|
120
|
+
if package_managers:
|
|
121
|
+
lines.append(f"Package manager(s): {', '.join(package_managers)}")
|
|
122
|
+
|
|
123
|
+
# Notable tools that are present, so the model can prefer them (and avoid
|
|
124
|
+
# suggesting ones that are missing).
|
|
125
|
+
candidate_tools = (
|
|
126
|
+
"rg",
|
|
127
|
+
"fd",
|
|
128
|
+
"fzf",
|
|
129
|
+
"jq",
|
|
130
|
+
"yq",
|
|
131
|
+
"git",
|
|
132
|
+
"curl",
|
|
133
|
+
"wget",
|
|
134
|
+
"docker",
|
|
135
|
+
"podman",
|
|
136
|
+
"kubectl",
|
|
137
|
+
"systemctl",
|
|
138
|
+
"tar",
|
|
139
|
+
"rsync",
|
|
140
|
+
"tmux",
|
|
141
|
+
"sed",
|
|
142
|
+
"awk",
|
|
143
|
+
)
|
|
144
|
+
available_tools = [tool for tool in candidate_tools if shutil.which(tool)]
|
|
145
|
+
if available_tools:
|
|
146
|
+
lines.append(f"Available tools: {', '.join(available_tools)}")
|
|
147
|
+
|
|
148
|
+
return "\n".join(lines)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def system_info() -> None:
|
|
152
|
+
"""Entry point for `ape-system-info`: print the detected system context."""
|
|
153
|
+
print(detect_system_context())
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def main() -> None:
|
|
157
|
+
"""Entry point for `ape`: suggest a command for the task in the arguments.
|
|
158
|
+
|
|
159
|
+
The query is taken from the command-line arguments (``sys.argv``). With no
|
|
160
|
+
arguments, the help text is printed and the program exits non-zero.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
args = sys.argv[1:]
|
|
164
|
+
if not args:
|
|
165
|
+
print(HELP)
|
|
166
|
+
raise SystemExit(1)
|
|
167
|
+
query = " ".join(args)
|
|
168
|
+
|
|
169
|
+
# The model is read from APE_MODEL, falling back to the default.
|
|
170
|
+
model = os.environ.get("APE_MODEL") or DEFAULT_MODEL
|
|
171
|
+
|
|
172
|
+
system_prompt = """\
|
|
173
|
+
You are a Linux command assistant. You will be asked a question about how to
|
|
174
|
+
perform a task in Linux or Unix-like operating systems. You should only include
|
|
175
|
+
in your answer the command or commands to perform the task. If you do not know how
|
|
176
|
+
to perform the task, output "echo "Please try again."".
|
|
177
|
+
|
|
178
|
+
It is important that you do not output commands enclosed in ``` ``` Markdown
|
|
179
|
+
blocks. For example, do not output:
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
cd projects
|
|
183
|
+
ls
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Instead, your output should be a command that is to be entered directly into the
|
|
187
|
+
command line. For the example above this is: cd projects && ls
|
|
188
|
+
|
|
189
|
+
You are also allowed to use \\ for command continuation.
|
|
190
|
+
|
|
191
|
+
Here are a few examples.
|
|
192
|
+
|
|
193
|
+
Question: List all the files and directories in projects in my home directory
|
|
194
|
+
Answer: ls ~/projects
|
|
195
|
+
|
|
196
|
+
Question: Navigate to projects and list its contents
|
|
197
|
+
Answer: cd projects && ls
|
|
198
|
+
|
|
199
|
+
Question: What is my username?
|
|
200
|
+
Answer: whoami
|
|
201
|
+
|
|
202
|
+
Question: Find all files with the extension .txt under the current working directory
|
|
203
|
+
Answer: find . -name "*.txt"
|
|
204
|
+
|
|
205
|
+
Question: What is the captial of France?
|
|
206
|
+
Answer: echo "Please try again."
|
|
207
|
+
|
|
208
|
+
Question: Tell me a story
|
|
209
|
+
Answer: echo "Please try again.\""""
|
|
210
|
+
|
|
211
|
+
# Append best-effort facts about the current machine so the model can
|
|
212
|
+
# tailor flags, package managers and tool choices to this environment.
|
|
213
|
+
system_context = detect_system_context()
|
|
214
|
+
if system_context:
|
|
215
|
+
system_prompt += (
|
|
216
|
+
"\n\n"
|
|
217
|
+
"Here are details about the current system. Prefer commands that are "
|
|
218
|
+
"correct for this environment (for example BSD vs GNU flags, and the "
|
|
219
|
+
"package manager and tools that are actually available):\n"
|
|
220
|
+
f"{system_context}"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
user_prompt = f"""\
|
|
224
|
+
Question: {query.strip()}
|
|
225
|
+
Answer:"""
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
answer = call_llm(model, system_prompt, user_prompt)
|
|
229
|
+
except ModelHTTPError as error:
|
|
230
|
+
print(f"{error.status_code} error: {error.message}", file=sys.stderr)
|
|
231
|
+
raise SystemExit(1)
|
|
232
|
+
except Exception as error:
|
|
233
|
+
# Anything else (missing/invalid credentials, unknown provider, etc.)
|
|
234
|
+
# surfaces as a one-line message rather than a traceback.
|
|
235
|
+
print(str(error), file=sys.stderr)
|
|
236
|
+
raise SystemExit(1)
|
|
237
|
+
|
|
238
|
+
if answer is None:
|
|
239
|
+
answer = 'echo "Please try again."'
|
|
240
|
+
print(answer)
|