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.
@@ -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: uv run pyright .
26
+ run: just type-check
25
27
  - name: Run tests
26
- run: uv run pytest
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: uv run ruff check .
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`).
@@ -1,4 +1,4 @@
1
- Copyright 2025 Seto Balian
1
+ Copyright 2026 Seto Balian
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to
@@ -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)
@@ -0,0 +1,11 @@
1
+ # Lint with ruff.
2
+ lint:
3
+ uv run ruff check .
4
+
5
+ # Type check with ty.
6
+ type-check:
7
+ uv run ty check
8
+
9
+ # Run the test suite.
10
+ test:
11
+ uv run pytest