ape-linux 0.4.0__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.4.0 → ape_linux-0.4.1}/CLAUDE.md +19 -18
- {ape_linux-0.4.0 → ape_linux-0.4.1}/PKG-INFO +7 -46
- {ape_linux-0.4.0 → ape_linux-0.4.1}/README.md +6 -43
- {ape_linux-0.4.0 → ape_linux-0.4.1}/ape_linux.py +38 -80
- {ape_linux-0.4.0 → ape_linux-0.4.1}/pyproject.toml +3 -4
- {ape_linux-0.4.0 → ape_linux-0.4.1}/tests/test_app.py +63 -66
- {ape_linux-0.4.0 → ape_linux-0.4.1}/uv.lock +2 -75
- {ape_linux-0.4.0 → ape_linux-0.4.1}/.github/workflows/publish.yaml +0 -0
- {ape_linux-0.4.0 → ape_linux-0.4.1}/.github/workflows/tests.yaml +0 -0
- {ape_linux-0.4.0 → ape_linux-0.4.1}/.gitignore +0 -0
- {ape_linux-0.4.0 → ape_linux-0.4.1}/.python-version +0 -0
- {ape_linux-0.4.0 → ape_linux-0.4.1}/LICENSE +0 -0
- {ape_linux-0.4.0 → ape_linux-0.4.1}/justfile +0 -0
|
@@ -11,7 +11,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
11
11
|
|
|
12
12
|
Ape (`ape-linux` on PyPI) is a CLI that turns a natural-language description of a
|
|
13
13
|
Linux task into a shell command using an LLM. The entire implementation lives in a
|
|
14
|
-
single module, `ape_linux.py`, exposing
|
|
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`.
|
|
15
17
|
|
|
16
18
|
**Single-file rule:** all of Ape's implementation must stay in `ape_linux.py` — do
|
|
17
19
|
not split it into additional modules/packages. New behavior is added as functions in
|
|
@@ -45,11 +47,12 @@ uv build # build the wheel/sdist
|
|
|
45
47
|
|
|
46
48
|
The flow in `ape_linux.py` is intentionally minimal:
|
|
47
49
|
|
|
48
|
-
1. `main()` is the
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`
|
|
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()`.
|
|
53
56
|
2. A hard-coded `system_prompt` (with few-shot examples) constrains the model to emit
|
|
54
57
|
**only** a runnable command — no Markdown fences — or `echo "Please try again."`
|
|
55
58
|
for anything off-topic. This prompt is the core product behavior; changes to it
|
|
@@ -68,23 +71,21 @@ The flow in `ape_linux.py` is intentionally minimal:
|
|
|
68
71
|
3. `call_llm()` wraps `pydantic_ai.Agent`, which is the provider abstraction. Models
|
|
69
72
|
are passed through verbatim in `provider:name` form (e.g. `anthropic:claude-sonnet-4-5`),
|
|
70
73
|
so Ape supports any provider Pydantic AI supports without provider-specific code.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
4. Errors are flattened to one-line stderr messages with exit code 1 —
|
|
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)` —
|
|
76
78
|
`ModelHTTPError` reports status/message; any other exception (bad credentials,
|
|
77
|
-
unknown provider) prints `str(error)`.
|
|
78
|
-
|
|
79
|
-
5. With `--execute`, the suggested command is run via `subprocess.check_call(..., shell=True)`.
|
|
79
|
+
unknown provider) prints `str(error)`. There is no CLI framework swallowing
|
|
80
|
+
tracebacks, so exceptions are caught explicitly.
|
|
80
81
|
|
|
81
82
|
## Testing
|
|
82
83
|
|
|
83
|
-
`tests/test_app.py`
|
|
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
|
|
84
87
|
`ape_linux.call_llm` so no real network/LLM calls happen. The `mockenv` fixture sets a
|
|
85
|
-
dummy `OPENAI_API_KEY`.
|
|
86
|
-
return `ls` and actually executes it (harmless, but be aware when adding execute-path
|
|
87
|
-
tests — avoid destructive mocked commands).
|
|
88
|
+
dummy `OPENAI_API_KEY`.
|
|
88
89
|
|
|
89
90
|
`detect_system_context()` is covered by tests that assert it returns a string without
|
|
90
91
|
crashing, reports the OS, and — importantly — excludes the current username, hostname,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ape-linux
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: AI for Linux commands
|
|
5
5
|
Project-URL: Homepage, https://github.com/sbalian/ape
|
|
6
6
|
Project-URL: Repository, https://github.com/sbalian/ape
|
|
@@ -15,8 +15,6 @@ Classifier: Programming Language :: Python
|
|
|
15
15
|
Classifier: Topic :: Software Development
|
|
16
16
|
Requires-Python: >=3.10
|
|
17
17
|
Requires-Dist: pydantic-ai-slim[anthropic,google,groq,mistral,openai]>=1.104.0
|
|
18
|
-
Requires-Dist: rich>=14.0.0
|
|
19
|
-
Requires-Dist: typer>=0.15.2
|
|
20
18
|
Description-Content-Type: text/markdown
|
|
21
19
|
|
|
22
20
|
# Ape
|
|
@@ -86,32 +84,16 @@ you should get:
|
|
|
86
84
|
echo "Please try again."
|
|
87
85
|
```
|
|
88
86
|
|
|
89
|
-
You can change the model
|
|
90
|
-
`provider:name` form.
|
|
87
|
+
You can change the model with the `APE_MODEL` environment variable. Models are
|
|
88
|
+
specified in `provider:name` form.
|
|
91
89
|
See [here](https://ai.pydantic.dev/models/) for the supported providers and models.
|
|
92
90
|
For example:
|
|
93
91
|
|
|
94
|
-
```bash
|
|
95
|
-
ape "List the contents of the working directory with as much detail as possible" --model anthropic:claude-sonnet-4-5
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
The model is resolved as follows:
|
|
99
|
-
|
|
100
|
-
1. If `--model`/`-m` is given, that value is always used.
|
|
101
|
-
2. Otherwise, if the `APE_MODEL` environment variable is set, its value is used.
|
|
102
|
-
3. Otherwise, the default `openai-chat:gpt-4.1` is used.
|
|
103
|
-
|
|
104
|
-
So you can set a personal default without passing `--model` every time:
|
|
105
|
-
|
|
106
92
|
```bash
|
|
107
93
|
export APE_MODEL=anthropic:claude-sonnet-4-5
|
|
108
94
|
```
|
|
109
95
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
```text
|
|
113
|
-
ls -lha
|
|
114
|
-
```
|
|
96
|
+
If `APE_MODEL` is unset, the default `openai-chat:gpt-4.1` is used.
|
|
115
97
|
|
|
116
98
|
## System-aware suggestions
|
|
117
99
|
|
|
@@ -131,11 +113,11 @@ This is all gathered locally with the Python standard library and is best-effort
|
|
|
131
113
|
anything can't be determined it is simply left out. **No identifying information is
|
|
132
114
|
collected or sent** — never your username, hostname, working directory, or home path.
|
|
133
115
|
|
|
134
|
-
To see exactly what Ape detects and sends (without calling the model),
|
|
135
|
-
|
|
116
|
+
To see exactly what Ape detects and sends (without calling the model), run
|
|
117
|
+
`ape-system-info`:
|
|
136
118
|
|
|
137
119
|
```bash
|
|
138
|
-
ape
|
|
120
|
+
ape-system-info
|
|
139
121
|
```
|
|
140
122
|
|
|
141
123
|
Output (example):
|
|
@@ -151,25 +133,4 @@ Package manager(s): brew
|
|
|
151
133
|
Available tools: rg, fd, jq, git, curl, docker, tar, rsync, sed, awk
|
|
152
134
|
```
|
|
153
135
|
|
|
154
|
-
## Executing commands
|
|
155
|
-
|
|
156
|
-
If you pass `--execute` or `-e`, the tool will run the command for you after printing it! Be careful with this as LLMs often make mistakes:
|
|
157
|
-
|
|
158
|
-
```bash
|
|
159
|
-
ape "Who am I logged in as?"
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
Output:
|
|
163
|
-
|
|
164
|
-
```text
|
|
165
|
-
whoami
|
|
166
|
-
jdoe
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
For more help:
|
|
170
|
-
|
|
171
|
-
```bash
|
|
172
|
-
ape --help
|
|
173
|
-
```
|
|
174
|
-
|
|
175
136
|
See also: [Gorilla](https://github.com/gorilla-llm)
|
|
@@ -65,32 +65,16 @@ you should get:
|
|
|
65
65
|
echo "Please try again."
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
You can change the model
|
|
69
|
-
`provider:name` form.
|
|
68
|
+
You can change the model with the `APE_MODEL` environment variable. Models are
|
|
69
|
+
specified in `provider:name` form.
|
|
70
70
|
See [here](https://ai.pydantic.dev/models/) for the supported providers and models.
|
|
71
71
|
For example:
|
|
72
72
|
|
|
73
|
-
```bash
|
|
74
|
-
ape "List the contents of the working directory with as much detail as possible" --model anthropic:claude-sonnet-4-5
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
The model is resolved as follows:
|
|
78
|
-
|
|
79
|
-
1. If `--model`/`-m` is given, that value is always used.
|
|
80
|
-
2. Otherwise, if the `APE_MODEL` environment variable is set, its value is used.
|
|
81
|
-
3. Otherwise, the default `openai-chat:gpt-4.1` is used.
|
|
82
|
-
|
|
83
|
-
So you can set a personal default without passing `--model` every time:
|
|
84
|
-
|
|
85
73
|
```bash
|
|
86
74
|
export APE_MODEL=anthropic:claude-sonnet-4-5
|
|
87
75
|
```
|
|
88
76
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
```text
|
|
92
|
-
ls -lha
|
|
93
|
-
```
|
|
77
|
+
If `APE_MODEL` is unset, the default `openai-chat:gpt-4.1` is used.
|
|
94
78
|
|
|
95
79
|
## System-aware suggestions
|
|
96
80
|
|
|
@@ -110,11 +94,11 @@ This is all gathered locally with the Python standard library and is best-effort
|
|
|
110
94
|
anything can't be determined it is simply left out. **No identifying information is
|
|
111
95
|
collected or sent** — never your username, hostname, working directory, or home path.
|
|
112
96
|
|
|
113
|
-
To see exactly what Ape detects and sends (without calling the model),
|
|
114
|
-
|
|
97
|
+
To see exactly what Ape detects and sends (without calling the model), run
|
|
98
|
+
`ape-system-info`:
|
|
115
99
|
|
|
116
100
|
```bash
|
|
117
|
-
ape
|
|
101
|
+
ape-system-info
|
|
118
102
|
```
|
|
119
103
|
|
|
120
104
|
Output (example):
|
|
@@ -130,25 +114,4 @@ Package manager(s): brew
|
|
|
130
114
|
Available tools: rg, fd, jq, git, curl, docker, tar, rsync, sed, awk
|
|
131
115
|
```
|
|
132
116
|
|
|
133
|
-
## Executing commands
|
|
134
|
-
|
|
135
|
-
If you pass `--execute` or `-e`, the tool will run the command for you after printing it! Be careful with this as LLMs often make mistakes:
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
ape "Who am I logged in as?"
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Output:
|
|
142
|
-
|
|
143
|
-
```text
|
|
144
|
-
whoami
|
|
145
|
-
jdoe
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
For more help:
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
|
-
ape --help
|
|
152
|
-
```
|
|
153
|
-
|
|
154
117
|
See also: [Gorilla](https://github.com/gorilla-llm)
|
|
@@ -3,32 +3,31 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import platform
|
|
5
5
|
import shutil
|
|
6
|
-
import
|
|
7
|
-
from importlib.metadata import version
|
|
8
|
-
from typing import Annotated
|
|
6
|
+
import sys
|
|
9
7
|
|
|
10
|
-
import rich.console
|
|
11
|
-
import typer
|
|
12
8
|
from pydantic_ai import Agent
|
|
13
9
|
from pydantic_ai.exceptions import ModelHTTPError
|
|
14
10
|
|
|
15
|
-
__version__ = version("ape_linux")
|
|
16
|
-
|
|
17
11
|
DEFAULT_MODEL = "openai-chat:gpt-4.1"
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
HELP = """\
|
|
14
|
+
ape — AI for Linux commands.
|
|
15
|
+
|
|
16
|
+
Usage: ape QUERY
|
|
20
17
|
|
|
18
|
+
Describe a Linux task in QUERY and ape prints a shell command for it.
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
raise typer.Exit()
|
|
20
|
+
Example:
|
|
21
|
+
ape "Create a symbolic link named 'win' pointing to /mnt/c/Users/jdoe"
|
|
22
|
+
ln -s /mnt/c/Users/jdoe win
|
|
26
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).
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
typer.echo(detect_system_context())
|
|
31
|
-
raise typer.Exit()
|
|
29
|
+
Run `ape-system-info` to print the detected system context sent to the model.\
|
|
30
|
+
""".format(default=DEFAULT_MODEL)
|
|
32
31
|
|
|
33
32
|
|
|
34
33
|
def call_llm(model: str, system_prompt: str, user_prompt: str) -> str | None:
|
|
@@ -149,64 +148,26 @@ def detect_system_context() -> str:
|
|
|
149
148
|
return "\n".join(lines)
|
|
150
149
|
|
|
151
150
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
help=(
|
|
163
|
-
"Model in provider:name form, e.g. anthropic:claude-sonnet-4-5. "
|
|
164
|
-
"See https://ai.pydantic.dev/models/. If unset, the APE_MODEL "
|
|
165
|
-
f"env var is used, falling back to {DEFAULT_MODEL}."
|
|
166
|
-
),
|
|
167
|
-
),
|
|
168
|
-
] = None,
|
|
169
|
-
execute: Annotated[
|
|
170
|
-
bool,
|
|
171
|
-
typer.Option(
|
|
172
|
-
"--execute",
|
|
173
|
-
"-e",
|
|
174
|
-
help="Run the command if suggested. Dangerous!",
|
|
175
|
-
),
|
|
176
|
-
] = False,
|
|
177
|
-
system_info: Annotated[
|
|
178
|
-
bool | None,
|
|
179
|
-
typer.Option(
|
|
180
|
-
"--system-info",
|
|
181
|
-
"-s",
|
|
182
|
-
callback=system_info_callback,
|
|
183
|
-
help="Show the detected system context sent to the model, then exit.",
|
|
184
|
-
is_eager=True,
|
|
185
|
-
),
|
|
186
|
-
] = None,
|
|
187
|
-
version: Annotated[
|
|
188
|
-
bool | None,
|
|
189
|
-
typer.Option(
|
|
190
|
-
"--version",
|
|
191
|
-
"-v",
|
|
192
|
-
callback=version_callback,
|
|
193
|
-
help="Show the version and exit.",
|
|
194
|
-
is_eager=True,
|
|
195
|
-
),
|
|
196
|
-
] = None,
|
|
197
|
-
):
|
|
198
|
-
"""Suggest a command for a Linux task described in QUERY.
|
|
199
|
-
|
|
200
|
-
Example: ape "Create a symbolic link named 'win' pointing to /mnt/c/Users/jdoe"
|
|
201
|
-
|
|
202
|
-
Output : ln -s /mnt/c/Users/jdoe win
|
|
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.
|
|
203
161
|
"""
|
|
204
162
|
|
|
205
|
-
|
|
163
|
+
args = sys.argv[1:]
|
|
164
|
+
if not args:
|
|
165
|
+
print(HELP)
|
|
166
|
+
raise SystemExit(1)
|
|
167
|
+
query = " ".join(args)
|
|
206
168
|
|
|
207
|
-
#
|
|
208
|
-
|
|
209
|
-
model = os.environ.get("APE_MODEL") or DEFAULT_MODEL
|
|
169
|
+
# The model is read from APE_MODEL, falling back to the default.
|
|
170
|
+
model = os.environ.get("APE_MODEL") or DEFAULT_MODEL
|
|
210
171
|
|
|
211
172
|
system_prompt = """\
|
|
212
173
|
You are a Linux command assistant. You will be asked a question about how to
|
|
@@ -264,19 +225,16 @@ def main(
|
|
|
264
225
|
Answer:"""
|
|
265
226
|
|
|
266
227
|
try:
|
|
267
|
-
|
|
268
|
-
answer = call_llm(model, system_prompt, user_prompt)
|
|
228
|
+
answer = call_llm(model, system_prompt, user_prompt)
|
|
269
229
|
except ModelHTTPError as error:
|
|
270
|
-
|
|
271
|
-
raise
|
|
230
|
+
print(f"{error.status_code} error: {error.message}", file=sys.stderr)
|
|
231
|
+
raise SystemExit(1)
|
|
272
232
|
except Exception as error:
|
|
273
233
|
# Anything else (missing/invalid credentials, unknown provider, etc.)
|
|
274
234
|
# surfaces as a one-line message rather than a traceback.
|
|
275
|
-
|
|
276
|
-
raise
|
|
235
|
+
print(str(error), file=sys.stderr)
|
|
236
|
+
raise SystemExit(1)
|
|
277
237
|
|
|
278
238
|
if answer is None:
|
|
279
239
|
answer = 'echo "Please try again."'
|
|
280
|
-
|
|
281
|
-
if execute:
|
|
282
|
-
subprocess.check_call(answer, shell=True)
|
|
240
|
+
print(answer)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ape-linux"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.1"
|
|
4
4
|
description = "AI for Linux commands"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = {text = "MIT License"}
|
|
@@ -20,8 +20,6 @@ classifiers = [
|
|
|
20
20
|
requires-python = ">=3.10"
|
|
21
21
|
dependencies = [
|
|
22
22
|
"pydantic-ai-slim[anthropic,google,groq,mistral,openai]>=1.104.0",
|
|
23
|
-
"rich>=14.0.0",
|
|
24
|
-
"typer>=0.15.2",
|
|
25
23
|
]
|
|
26
24
|
|
|
27
25
|
[project.urls]
|
|
@@ -29,7 +27,8 @@ Homepage = "https://github.com/sbalian/ape"
|
|
|
29
27
|
Repository = "https://github.com/sbalian/ape"
|
|
30
28
|
|
|
31
29
|
[project.scripts]
|
|
32
|
-
ape = "ape_linux:
|
|
30
|
+
ape = "ape_linux:main"
|
|
31
|
+
ape-system-info = "ape_linux:system_info"
|
|
33
32
|
|
|
34
33
|
[build-system]
|
|
35
34
|
requires = ["hatchling"]
|
|
@@ -3,61 +3,86 @@ import os
|
|
|
3
3
|
import socket
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
|
-
import typer.testing
|
|
7
6
|
from pydantic_ai.exceptions import ModelHTTPError
|
|
8
7
|
|
|
9
8
|
import ape_linux
|
|
10
9
|
|
|
11
|
-
runner = typer.testing.CliRunner()
|
|
12
|
-
|
|
13
10
|
|
|
14
11
|
@pytest.fixture
|
|
15
12
|
def mockenv(monkeypatch):
|
|
16
13
|
monkeypatch.setenv("OPENAI_API_KEY", "key")
|
|
17
14
|
|
|
18
15
|
|
|
19
|
-
def
|
|
16
|
+
def run(monkeypatch, argv):
|
|
17
|
+
"""Run ape_linux.main() with the given argv, returning the SystemExit code.
|
|
18
|
+
|
|
19
|
+
main() never calls sys.exit() on the success path, so a clean run returns 0.
|
|
20
|
+
"""
|
|
21
|
+
monkeypatch.setattr("sys.argv", ["ape", *argv])
|
|
22
|
+
try:
|
|
23
|
+
ape_linux.main()
|
|
24
|
+
except SystemExit as exit:
|
|
25
|
+
return exit.code or 0
|
|
26
|
+
return 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_app_for_suggestion(mockenv, monkeypatch, capsys):
|
|
20
30
|
monkeypatch.setattr("ape_linux.call_llm", lambda *args, **kwargs: "ls")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
assert
|
|
24
|
-
assert
|
|
31
|
+
code = run(monkeypatch, ["list all the files"])
|
|
32
|
+
captured = capsys.readouterr()
|
|
33
|
+
assert captured.out == "ls\n"
|
|
34
|
+
assert captured.err == ""
|
|
35
|
+
assert code == 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_app_joins_multiple_args_into_query(mockenv, monkeypatch, capsys):
|
|
39
|
+
captured_prompt = {}
|
|
40
|
+
|
|
41
|
+
def mockreturn(model, system_prompt, user_prompt):
|
|
42
|
+
captured_prompt["user"] = user_prompt
|
|
43
|
+
return "ls"
|
|
44
|
+
|
|
45
|
+
monkeypatch.setattr("ape_linux.call_llm", mockreturn)
|
|
46
|
+
run(monkeypatch, ["list", "all", "the", "files"])
|
|
47
|
+
assert "list all the files" in captured_prompt["user"]
|
|
48
|
+
|
|
25
49
|
|
|
50
|
+
def test_app_prints_help_and_exits_when_no_args(monkeypatch, capsys):
|
|
51
|
+
code = run(monkeypatch, [])
|
|
52
|
+
captured = capsys.readouterr()
|
|
53
|
+
assert "Usage: ape QUERY" in captured.out
|
|
54
|
+
assert code == 1
|
|
26
55
|
|
|
27
|
-
|
|
56
|
+
|
|
57
|
+
def test_app_for_try_again_if_api_returns_none(mockenv, monkeypatch, capsys):
|
|
28
58
|
monkeypatch.setattr("ape_linux.call_llm", lambda *args, **kwargs: None)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
assert
|
|
32
|
-
assert
|
|
59
|
+
code = run(monkeypatch, ["what is the capital of England?"])
|
|
60
|
+
captured = capsys.readouterr()
|
|
61
|
+
assert captured.out == 'echo "Please try again."\n'
|
|
62
|
+
assert captured.err == ""
|
|
63
|
+
assert code == 0
|
|
33
64
|
|
|
34
65
|
|
|
35
|
-
def test_app_with_api_error(mockenv, monkeypatch):
|
|
66
|
+
def test_app_with_api_error(mockenv, monkeypatch, capsys):
|
|
36
67
|
def mockreturn(*args, **kwargs):
|
|
37
68
|
raise ModelHTTPError(
|
|
38
69
|
status_code=500, model_name="openai-chat:gpt-4o", body=None
|
|
39
70
|
)
|
|
40
71
|
|
|
41
72
|
monkeypatch.setattr("ape_linux.call_llm", mockreturn)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
assert
|
|
73
|
+
code = run(monkeypatch, ["list all the files"])
|
|
74
|
+
captured = capsys.readouterr()
|
|
75
|
+
assert captured.out == ""
|
|
76
|
+
assert code == 1
|
|
45
77
|
|
|
46
78
|
|
|
47
|
-
def test_app_with_no_api_key(mockenv, monkeypatch):
|
|
79
|
+
def test_app_with_no_api_key(mockenv, monkeypatch, capsys):
|
|
48
80
|
monkeypatch.delenv("OPENAI_API_KEY")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
assert
|
|
52
|
-
assert
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_app_for_suggestion_with_execute(mockenv, monkeypatch):
|
|
56
|
-
monkeypatch.setattr("ape_linux.call_llm", lambda *args, **kwargs: "ls")
|
|
57
|
-
result = runner.invoke(ape_linux.app, ["list all the files", "--execute"])
|
|
58
|
-
assert result.stdout == "ls\n" # careful, this will actually run
|
|
59
|
-
assert result.stderr == ""
|
|
60
|
-
assert result.exit_code == 0
|
|
81
|
+
code = run(monkeypatch, ["list all the files"])
|
|
82
|
+
captured = capsys.readouterr()
|
|
83
|
+
assert captured.out == ""
|
|
84
|
+
assert captured.err != ""
|
|
85
|
+
assert code == 1
|
|
61
86
|
|
|
62
87
|
|
|
63
88
|
def test_app_uses_default_model_when_unset(mockenv, monkeypatch):
|
|
@@ -69,10 +94,10 @@ def test_app_uses_default_model_when_unset(mockenv, monkeypatch):
|
|
|
69
94
|
|
|
70
95
|
monkeypatch.delenv("APE_MODEL", raising=False)
|
|
71
96
|
monkeypatch.setattr("ape_linux.call_llm", mockreturn)
|
|
72
|
-
|
|
97
|
+
code = run(monkeypatch, ["list all the files"])
|
|
73
98
|
assert captured["model"] == ape_linux.DEFAULT_MODEL
|
|
74
99
|
assert ape_linux.DEFAULT_MODEL == "openai-chat:gpt-4.1"
|
|
75
|
-
assert
|
|
100
|
+
assert code == 0
|
|
76
101
|
|
|
77
102
|
|
|
78
103
|
def test_app_uses_ape_model_env_var(mockenv, monkeypatch):
|
|
@@ -84,43 +109,15 @@ def test_app_uses_ape_model_env_var(mockenv, monkeypatch):
|
|
|
84
109
|
|
|
85
110
|
monkeypatch.setenv("APE_MODEL", "anthropic:claude-sonnet-4-5")
|
|
86
111
|
monkeypatch.setattr("ape_linux.call_llm", mockreturn)
|
|
87
|
-
|
|
112
|
+
code = run(monkeypatch, ["list all the files"])
|
|
88
113
|
assert captured["model"] == "anthropic:claude-sonnet-4-5"
|
|
89
|
-
assert
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def test_app_model_option_overrides_env_var(mockenv, monkeypatch):
|
|
93
|
-
captured = {}
|
|
94
|
-
|
|
95
|
-
def mockreturn(model, *args, **kwargs):
|
|
96
|
-
captured["model"] = model
|
|
97
|
-
return "ls"
|
|
98
|
-
|
|
99
|
-
monkeypatch.setenv("APE_MODEL", "anthropic:claude-sonnet-4-5")
|
|
100
|
-
monkeypatch.setattr("ape_linux.call_llm", mockreturn)
|
|
101
|
-
result = runner.invoke(
|
|
102
|
-
ape_linux.app, ["list all the files", "--model", "groq:llama-3.3-70b"]
|
|
103
|
-
)
|
|
104
|
-
assert captured["model"] == "groq:llama-3.3-70b"
|
|
105
|
-
assert result.exit_code == 0
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def test_app_for_version(mockenv):
|
|
109
|
-
result = runner.invoke(ape_linux.app, ["--version"])
|
|
110
|
-
assert result.stdout == f"{ape_linux.__version__}\n"
|
|
111
|
-
assert result.stderr == ""
|
|
112
|
-
assert result.exit_code == 0
|
|
113
|
-
|
|
114
|
+
assert code == 0
|
|
114
115
|
|
|
115
|
-
def test_app_system_info_flag(mockenv, monkeypatch):
|
|
116
|
-
# The flag must print the context and exit without ever calling the LLM.
|
|
117
|
-
def fail(*args, **kwargs):
|
|
118
|
-
raise AssertionError("call_llm should not be invoked for --system-info")
|
|
119
116
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
assert
|
|
117
|
+
def test_system_info_entry_point(capsys):
|
|
118
|
+
ape_linux.system_info()
|
|
119
|
+
captured = capsys.readouterr()
|
|
120
|
+
assert captured.out == f"{ape_linux.detect_system_context()}\n"
|
|
124
121
|
|
|
125
122
|
|
|
126
123
|
def test_detect_system_context_returns_str_and_never_crashes():
|
|
@@ -2,15 +2,6 @@ version = 1
|
|
|
2
2
|
revision = 3
|
|
3
3
|
requires-python = ">=3.10"
|
|
4
4
|
|
|
5
|
-
[[package]]
|
|
6
|
-
name = "annotated-doc"
|
|
7
|
-
version = "0.0.4"
|
|
8
|
-
source = { registry = "https://pypi.org/simple" }
|
|
9
|
-
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
|
10
|
-
wheels = [
|
|
11
|
-
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
|
12
|
-
]
|
|
13
|
-
|
|
14
5
|
[[package]]
|
|
15
6
|
name = "annotated-types"
|
|
16
7
|
version = "0.7.0"
|
|
@@ -55,12 +46,10 @@ wheels = [
|
|
|
55
46
|
|
|
56
47
|
[[package]]
|
|
57
48
|
name = "ape-linux"
|
|
58
|
-
version = "0.4.
|
|
49
|
+
version = "0.4.1"
|
|
59
50
|
source = { editable = "." }
|
|
60
51
|
dependencies = [
|
|
61
52
|
{ name = "pydantic-ai-slim", extra = ["anthropic", "google", "groq", "mistral", "openai"] },
|
|
62
|
-
{ name = "rich" },
|
|
63
|
-
{ name = "typer" },
|
|
64
53
|
]
|
|
65
54
|
|
|
66
55
|
[package.dev-dependencies]
|
|
@@ -71,11 +60,7 @@ dev = [
|
|
|
71
60
|
]
|
|
72
61
|
|
|
73
62
|
[package.metadata]
|
|
74
|
-
requires-dist = [
|
|
75
|
-
{ name = "pydantic-ai-slim", extras = ["anthropic", "google", "groq", "mistral", "openai"], specifier = ">=1.104.0" },
|
|
76
|
-
{ name = "rich", specifier = ">=14.0.0" },
|
|
77
|
-
{ name = "typer", specifier = ">=0.15.2" },
|
|
78
|
-
]
|
|
63
|
+
requires-dist = [{ name = "pydantic-ai-slim", extras = ["anthropic", "google", "groq", "mistral", "openai"], specifier = ">=1.104.0" }]
|
|
79
64
|
|
|
80
65
|
[package.metadata.requires-dev]
|
|
81
66
|
dev = [
|
|
@@ -682,27 +667,6 @@ wheels = [
|
|
|
682
667
|
{ url = "https://files.pythonhosted.org/packages/97/b9/639818adb83dad11e9b21b4ad1906883b98732b06959257e33f7d7f14db9/logfire_api-4.34.0-py3-none-any.whl", hash = "sha256:ca51498bb60e1e4505c90cc20f949d2d6dcf9d15cf5fd5a46f14016f2f638c4c", size = 130718, upload-time = "2026-05-26T18:09:50.551Z" },
|
|
683
668
|
]
|
|
684
669
|
|
|
685
|
-
[[package]]
|
|
686
|
-
name = "markdown-it-py"
|
|
687
|
-
version = "4.2.0"
|
|
688
|
-
source = { registry = "https://pypi.org/simple" }
|
|
689
|
-
dependencies = [
|
|
690
|
-
{ name = "mdurl" },
|
|
691
|
-
]
|
|
692
|
-
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
|
|
693
|
-
wheels = [
|
|
694
|
-
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
|
695
|
-
]
|
|
696
|
-
|
|
697
|
-
[[package]]
|
|
698
|
-
name = "mdurl"
|
|
699
|
-
version = "0.1.2"
|
|
700
|
-
source = { registry = "https://pypi.org/simple" }
|
|
701
|
-
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
|
702
|
-
wheels = [
|
|
703
|
-
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
|
704
|
-
]
|
|
705
|
-
|
|
706
670
|
[[package]]
|
|
707
671
|
name = "mistralai"
|
|
708
672
|
version = "2.4.8"
|
|
@@ -1173,19 +1137,6 @@ wheels = [
|
|
|
1173
1137
|
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
|
1174
1138
|
]
|
|
1175
1139
|
|
|
1176
|
-
[[package]]
|
|
1177
|
-
name = "rich"
|
|
1178
|
-
version = "15.0.0"
|
|
1179
|
-
source = { registry = "https://pypi.org/simple" }
|
|
1180
|
-
dependencies = [
|
|
1181
|
-
{ name = "markdown-it-py" },
|
|
1182
|
-
{ name = "pygments" },
|
|
1183
|
-
]
|
|
1184
|
-
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
|
1185
|
-
wheels = [
|
|
1186
|
-
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
|
1187
|
-
]
|
|
1188
|
-
|
|
1189
1140
|
[[package]]
|
|
1190
1141
|
name = "ruff"
|
|
1191
1142
|
version = "0.15.15"
|
|
@@ -1211,15 +1162,6 @@ wheels = [
|
|
|
1211
1162
|
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
|
|
1212
1163
|
]
|
|
1213
1164
|
|
|
1214
|
-
[[package]]
|
|
1215
|
-
name = "shellingham"
|
|
1216
|
-
version = "1.5.4"
|
|
1217
|
-
source = { registry = "https://pypi.org/simple" }
|
|
1218
|
-
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
|
1219
|
-
wheels = [
|
|
1220
|
-
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
|
1221
|
-
]
|
|
1222
|
-
|
|
1223
1165
|
[[package]]
|
|
1224
1166
|
name = "six"
|
|
1225
1167
|
version = "1.17.0"
|
|
@@ -1399,21 +1341,6 @@ wheels = [
|
|
|
1399
1341
|
{ url = "https://files.pythonhosted.org/packages/04/87/369056ed46f1b235130ec0595393262f9cd2061ca3dab276d490980f9343/ty-0.0.40-py3-none-win_arm64.whl", hash = "sha256:07da2b09d9130e2c9a257d2a29beb53105835b0256ee5fdb288fe1aab83fee47", size = 11117369, upload-time = "2026-05-27T17:55:39.329Z" },
|
|
1400
1342
|
]
|
|
1401
1343
|
|
|
1402
|
-
[[package]]
|
|
1403
|
-
name = "typer"
|
|
1404
|
-
version = "0.26.4"
|
|
1405
|
-
source = { registry = "https://pypi.org/simple" }
|
|
1406
|
-
dependencies = [
|
|
1407
|
-
{ name = "annotated-doc" },
|
|
1408
|
-
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
1409
|
-
{ name = "rich" },
|
|
1410
|
-
{ name = "shellingham" },
|
|
1411
|
-
]
|
|
1412
|
-
sdist = { url = "https://files.pythonhosted.org/packages/8e/d3/90c1ee19209cb59f6ad185883fd4ccfcf72f8f0bfd549d5a8b70474611d0/typer-0.26.4.tar.gz", hash = "sha256:25b128964de66c5ea36d5ac82adc579e5e113509b17469edf9f5a4a1864ff2a9", size = 201191, upload-time = "2026-05-30T17:05:04.213Z" }
|
|
1413
|
-
wheels = [
|
|
1414
|
-
{ url = "https://files.pythonhosted.org/packages/f0/6d/5a525c69df4a90892135e5d490b00e9e46402491f3416d4395fcb0d0201e/typer-0.26.4-py3-none-any.whl", hash = "sha256:11bfd7b43557137e373c2b10f6967a555f9678a61ed72c808968b011d95534d6", size = 122436, upload-time = "2026-05-30T17:05:05.812Z" },
|
|
1415
|
-
]
|
|
1416
|
-
|
|
1417
1344
|
[[package]]
|
|
1418
1345
|
name = "typing-extensions"
|
|
1419
1346
|
version = "4.15.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|