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.
@@ -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 a Typer app as the `ape` console script.
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 single Typer command. It takes the `query` argument plus
49
- `--model/-m`, `--execute/-e`, `--system-info/-s`, and `--version/-v` options.
50
- `--system-info` is an eager flag (like `--version`) whose callback prints
51
- `detect_system_context()` and exits before the LLM is called, so it works without a
52
- `query`.
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
- `--model` defaults to `None`; `main()` resolves the model as `--model` (if given)
72
- → `APE_MODEL` env var → the `DEFAULT_MODEL` constant (`openai-chat:gpt-4.1`).
73
- Credentials come from each provider's standard env var (e.g. `OPENAI_API_KEY`,
74
- `ANTHROPIC_API_KEY`).
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)`. Tracebacks are suppressed via
78
- `pretty_exceptions_enable=False` on the Typer app.
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` uses `typer.testing.CliRunner` and monkeypatches
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`. Note `test_app_for_suggestion_with_execute` mocks the LLM to
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.0
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 using `--model` or `-m`. Models are specified in
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
- Output:
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), use
135
- `--system-info` or `-s`:
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 --system-info
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 using `--model` or `-m`. Models are specified in
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
- Output:
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), use
114
- `--system-info` or `-s`:
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 --system-info
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 subprocess
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
- app = typer.Typer(add_completion=False, pretty_exceptions_enable=False)
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
- def version_callback(value: bool) -> None:
23
- if value:
24
- typer.echo(__version__)
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
- def system_info_callback(value: bool) -> None:
29
- if value:
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
- @app.command()
153
- def main(
154
- query: Annotated[
155
- str, typer.Argument(help="Query describing a Linux task.", show_default=False)
156
- ],
157
- model: Annotated[
158
- str | None,
159
- typer.Option(
160
- "--model",
161
- "-m",
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
- console = rich.console.Console()
163
+ args = sys.argv[1:]
164
+ if not args:
165
+ print(HELP)
166
+ raise SystemExit(1)
167
+ query = " ".join(args)
206
168
 
207
- # An explicit --model wins. Otherwise fall back to APE_MODEL, then the default.
208
- if model is None:
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
- with console.status("[bold][blue]Processing ...", spinner="monkey"):
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
- typer.echo(f"{error.status_code} error: {error.message}", err=True)
271
- raise typer.Exit(1)
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
- typer.echo(str(error), err=True)
276
- raise typer.Exit(1)
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
- typer.echo(answer)
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.0"
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:app"
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 test_app_for_suggestion(mockenv, monkeypatch):
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
- result = runner.invoke(ape_linux.app, ["list all the files"])
22
- assert result.stdout == "ls\n"
23
- assert result.stderr == ""
24
- assert result.exit_code == 0
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
- def test_app_for_try_again_if_api_returns_none(mockenv, monkeypatch):
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
- result = runner.invoke(ape_linux.app, ["what is the capital of England?"])
30
- assert result.stdout == 'echo "Please try again."\n'
31
- assert result.stderr == ""
32
- assert result.exit_code == 0
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
- result = runner.invoke(ape_linux.app, ["list all the files"])
43
- assert result.stdout == ""
44
- assert result.exit_code == 1
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
- result = runner.invoke(ape_linux.app, ["list all the files"])
50
- assert result.stdout == ""
51
- assert result.stderr != ""
52
- assert result.exit_code == 1
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
- result = runner.invoke(ape_linux.app, ["list all the files"])
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 result.exit_code == 0
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
- result = runner.invoke(ape_linux.app, ["list all the files"])
112
+ code = run(monkeypatch, ["list all the files"])
88
113
  assert captured["model"] == "anthropic:claude-sonnet-4-5"
89
- assert result.exit_code == 0
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
- monkeypatch.setattr("ape_linux.call_llm", fail)
121
- result = runner.invoke(ape_linux.app, ["--system-info"])
122
- assert result.stdout == f"{ape_linux.detect_system_context()}\n"
123
- assert result.exit_code == 0
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.0"
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