lex-the-hacker 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: dev
3
+ description: Development commands and workflow for the lex-the-hacker project — running the CLI, running tests (including a single test), and managing dependencies with uv. Use when working on this repo and you need the project's build/test/run commands.
4
+ ---
5
+
6
+ # lex-the-hacker dev workflow
7
+
8
+ This project is a [Click](https://click.palletsprojects.com/) CLI managed with [`uv`](https://docs.astral.sh/uv/) (Python >=3.13). All commands run through `uv`, which manages the virtual environment automatically — there's no need to activate `.venv` manually.
9
+
10
+ ## Tests
11
+
12
+ - Run all tests: `uv run pytest`
13
+ - Verbose: `uv run pytest -v`
14
+ - Run a single file: `uv run pytest tests/test_cli.py`
15
+ - Run a single test: `uv run pytest tests/test_cli.py::test_version`
16
+ - Run tests matching a keyword: `uv run pytest -k version`
17
+
18
+ Tests live in `tests/` and use Click's `CliRunner` to invoke the command in-process.
19
+
20
+ ## Running the CLI
21
+
22
+ - Show help: `uv run lex --help`
23
+ - Show version: `uv run lex --version`
24
+
25
+ The `lex` command is defined as a console script in `pyproject.toml` (`[project.scripts]` → `lex = "lex_the_hacker.cli:cli"`).
26
+
27
+ ## Dependencies
28
+
29
+ - Add a runtime dependency: `uv add <package>`
30
+ - Add a dev-only dependency: `uv add --dev <package>`
31
+ - Remove a dependency: `uv remove <package>`
32
+ - Sync the environment from the lockfile: `uv sync`
33
+
34
+ ## Layout
35
+
36
+ - `lex_the_hacker/cli.py` — the Click command (the CLI entry point).
37
+ - `lex_the_hacker/__init__.py` — holds `__version__` (the single source of truth for the version, wired into `--version`).
38
+ - `tests/` — pytest tests.
39
+
40
+ ## Version bumps
41
+
42
+ The version lives in two places that must stay in sync: `__version__` in `lex_the_hacker/__init__.py` and `version` in `pyproject.toml`. Update both when releasing.
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,24 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project state
6
+
7
+ Early-stage [`uv`](https://docs.astral.sh/uv/) project (Python >=3.13). It's a [Click](https://click.palletsprojects.com/) CLI exposing a `lex` command group: `lex "<description>"` generates a shell command via an LLM (any OpenAI-compatible API, configured through `LEX_MODEL` / `LEX_API_KEY` / `LEX_BASE_URL`), shows an explanation, offers the command in a [questionary](https://questionary.readthedocs.io/) select (choosing it copies it to the clipboard), and puts it on the user's prompt without running it; `lex shell` shows the detected shell (plus `--version` / `--help`).
8
+
9
+ ## Layout
10
+
11
+ - `lex_the_hacker/cli.py` — the Click command group and its subcommands; entry point is `lex = "lex_the_hacker.cli:cli"` (see `[project.scripts]` in `pyproject.toml`). The UI ([rich](https://rich.readthedocs.io/) spinner and command/explanation panel) renders on stderr; the questionary select (interactive terminals only — see `_can_prompt`) draws on stdout; clipboard copy is pyperclip with an OSC 52 terminal-escape fallback.
12
+ - `lex_the_hacker/agent.py` — the [pydantic-ai](https://ai.pydantic.dev/) agents: a gate agent that rejects non-command requests, and a generate agent returning a command plus explanation. Model is built per run from the `LEX_*` env vars.
13
+ - `lex_the_hacker/buffer.py` — puts the generated command on the user's prompt via the `TIOCSTI` ioctl; returns False (silently — the panel already shows the command) when it can't, and never injects a newline (that would execute the command).
14
+ - `lex_the_hacker/shell.py` — shell detection via [`shellingham`](https://pypi.org/project/shellingham/); exposes `ShellInfo`, `detect()`, and `config_path_for()`, and backs `lex shell`.
15
+ - `lex_the_hacker/__init__.py` — holds `__version__`, the single source of truth for the version (wired into `--version`).
16
+ - `tests/` — pytest, using Click's `CliRunner` to invoke the command. CLI tests use the `_ask` helper to stub `cli._copy_to_clipboard` (so the suite never touches the real clipboard) and `cli._can_prompt` / `cli._chose_command` (questionary needs a real terminal).
17
+
18
+ ## Commands
19
+
20
+ - Run the CLI: `uv run lex --help`
21
+ - Run tests: `uv run pytest`
22
+ - Run a single test: `uv run pytest tests/test_cli.py::test_version`
23
+ - Add a dependency: `uv add <package>` (dev-only: `uv add --dev <package>`)
24
+ - Sync the environment from the lockfile: `uv sync`
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.5
2
+ Name: lex-the-hacker
3
+ Version: 0.1.0
4
+ Summary: An AI CLI that writes shell commands for you — and never runs them.
5
+ Requires-Python: >=3.13
6
+ Requires-Dist: click>=8.5.0
7
+ Requires-Dist: pydantic-ai-slim[openai]>=2.35.0
8
+ Requires-Dist: pyperclip>=1.11.0
9
+ Requires-Dist: questionary>=2.1.1
10
+ Requires-Dist: rich>=15.0.0
11
+ Requires-Dist: shellingham>=1.5.4
12
+ Description-Content-Type: text/markdown
13
+
14
+ # lex the hacker
15
+
16
+ > *I prefer to be called a hacker.*
17
+
18
+ ![It's a UNIX system! I know this!](docs/images/lex.jpeg)
19
+
20
+ Describe what you want in plain English, and `lex` writes the shell command for you.
21
+
22
+ ```
23
+ $ lex "search the dir for 'xyz'"
24
+ ```
25
+
26
+ ```
27
+ $ grep -rn 'xyz' .
28
+ ```
29
+
30
+ While `lex` thinks, you get a spinner. Then it shows the command with a short
31
+ explanation of how it works, and offers it to you — pick it (just press Enter)
32
+ to copy it to your clipboard:
33
+
34
+ ```
35
+ ╭──────────────────────────────────────────────────╮
36
+ │ grep -rn 'xyz' . │
37
+ │ │
38
+ │ Searches every file under the current directory │
39
+ │ for the text "xyz", printing each match with its │
40
+ │ file name and line number. │
41
+ ╰──────────────────────────────────────────────────╯
42
+ ? copy to clipboard? (Use arrow keys)
43
+ » grep -rn 'xyz' .
44
+ skip
45
+
46
+ ✓ copied to clipboard
47
+ ```
48
+
49
+ Either way, the command also lands on your command line, ready to run.
50
+
51
+ It **never executes anything itself**. You always read the command and press
52
+ Enter (or edit it, or throw it away). Nothing runs without you.
53
+
54
+ > **On typing.** Putting text on your prompt uses the `TIOCSTI` ioctl, which
55
+ > current Linux kernels disable by default (`dev.tty.legacy_tiocsti = 0`)
56
+ > because it was a privilege-escalation vector. When it's unavailable — a
57
+ > hardened kernel, no terminal, a pipe — or when the command spans more than
58
+ > one line, the command simply doesn't land on your prompt. That's a normal
59
+ > outcome, not an error: it's still right there in the panel, and on your
60
+ > clipboard. Either way, `lex` never presses Enter for you.
61
+
62
+ ## Install
63
+
64
+ ```
65
+ pip install lex-the-hacker
66
+ ```
67
+
68
+ This gives you the `lex` command.
69
+
70
+ ## Configuration
71
+
72
+ `lex` talks to any OpenAI-compatible chat API — hosted OpenAI, or a local
73
+ [Ollama](https://ollama.com) server. Point it at one with three environment
74
+ variables:
75
+
76
+ | Variable | Required | What it is |
77
+ | --- | --- | --- |
78
+ | `LEX_MODEL` | yes | The model name to request, e.g. `gpt-4o-mini` or `llama3.1`. |
79
+ | `LEX_API_KEY` | yes | The API key. Ollama ignores it, but the client still requires a non-empty value — `ollama` works fine. |
80
+ | `LEX_BASE_URL` | no | Base URL of the endpoint. Defaults to OpenAI's hosted API. |
81
+
82
+ Hosted OpenAI:
83
+
84
+ ```
85
+ export LEX_MODEL=gpt-4o-mini
86
+ export LEX_API_KEY=sk-...
87
+ ```
88
+
89
+ A local Ollama server:
90
+
91
+ ```
92
+ export LEX_MODEL=llama3.1
93
+ export LEX_API_KEY=ollama
94
+ export LEX_BASE_URL=http://localhost:11434/v1
95
+ ```
96
+
97
+ `lex` also tells the model which shell you're in, so the command it writes
98
+ matches your shell's syntax. Run `lex shell` to see what it detected.
99
+
100
+ > **Clipboard on Linux.** Copying first tries your system clipboard via
101
+ > [pyperclip](https://pypi.org/project/pyperclip/), which on Linux needs one of
102
+ > `xclip`, `xsel`, or `wl-clipboard` installed. Without one, `lex` falls back
103
+ > to OSC 52 — asking your terminal itself to set the clipboard, no install
104
+ > needed — which modern terminals
105
+ > (VS Code, kitty, alacritty, WezTerm, iTerm2, foot, …) support, even over
106
+ > SSH. If neither route works, the copy is skipped quietly.
107
+
108
+ ## Usage
109
+
110
+ Call `lex` with a description of what you want to do, in quotes:
111
+
112
+ ```
113
+ $ lex "find every python file changed in the last week"
114
+ ```
115
+
116
+ `lex` puts its best guess on your prompt:
117
+
118
+ ```
119
+ $ find . -name '*.py' -mtime -7
120
+ ```
121
+
122
+ From there it's your command like any other — run it, tweak it, or clear the line and try a different description.
123
+
124
+ More examples:
125
+
126
+ ```
127
+ $ lex "undo my last git commit but keep the changes"
128
+ $ git reset --soft HEAD~1
129
+
130
+ $ lex "compress this folder into a tar.gz"
131
+ $ tar -czf folder.tar.gz folder/
132
+
133
+ $ lex "show what's listening on port 8080"
134
+ $ lsof -i :8080
135
+ ```
136
+
137
+ ## Why you're always safe
138
+
139
+ `lex` only ever *writes* a command to your shell. It doesn't run it, and it can't run it — executing is up to you. So if a suggestion looks wrong, you'll see it before anything happens.
@@ -0,0 +1,126 @@
1
+ # lex the hacker
2
+
3
+ > *I prefer to be called a hacker.*
4
+
5
+ ![It's a UNIX system! I know this!](docs/images/lex.jpeg)
6
+
7
+ Describe what you want in plain English, and `lex` writes the shell command for you.
8
+
9
+ ```
10
+ $ lex "search the dir for 'xyz'"
11
+ ```
12
+
13
+ ```
14
+ $ grep -rn 'xyz' .
15
+ ```
16
+
17
+ While `lex` thinks, you get a spinner. Then it shows the command with a short
18
+ explanation of how it works, and offers it to you — pick it (just press Enter)
19
+ to copy it to your clipboard:
20
+
21
+ ```
22
+ ╭──────────────────────────────────────────────────╮
23
+ │ grep -rn 'xyz' . │
24
+ │ │
25
+ │ Searches every file under the current directory │
26
+ │ for the text "xyz", printing each match with its │
27
+ │ file name and line number. │
28
+ ╰──────────────────────────────────────────────────╯
29
+ ? copy to clipboard? (Use arrow keys)
30
+ » grep -rn 'xyz' .
31
+ skip
32
+
33
+ ✓ copied to clipboard
34
+ ```
35
+
36
+ Either way, the command also lands on your command line, ready to run.
37
+
38
+ It **never executes anything itself**. You always read the command and press
39
+ Enter (or edit it, or throw it away). Nothing runs without you.
40
+
41
+ > **On typing.** Putting text on your prompt uses the `TIOCSTI` ioctl, which
42
+ > current Linux kernels disable by default (`dev.tty.legacy_tiocsti = 0`)
43
+ > because it was a privilege-escalation vector. When it's unavailable — a
44
+ > hardened kernel, no terminal, a pipe — or when the command spans more than
45
+ > one line, the command simply doesn't land on your prompt. That's a normal
46
+ > outcome, not an error: it's still right there in the panel, and on your
47
+ > clipboard. Either way, `lex` never presses Enter for you.
48
+
49
+ ## Install
50
+
51
+ ```
52
+ pip install lex-the-hacker
53
+ ```
54
+
55
+ This gives you the `lex` command.
56
+
57
+ ## Configuration
58
+
59
+ `lex` talks to any OpenAI-compatible chat API — hosted OpenAI, or a local
60
+ [Ollama](https://ollama.com) server. Point it at one with three environment
61
+ variables:
62
+
63
+ | Variable | Required | What it is |
64
+ | --- | --- | --- |
65
+ | `LEX_MODEL` | yes | The model name to request, e.g. `gpt-4o-mini` or `llama3.1`. |
66
+ | `LEX_API_KEY` | yes | The API key. Ollama ignores it, but the client still requires a non-empty value — `ollama` works fine. |
67
+ | `LEX_BASE_URL` | no | Base URL of the endpoint. Defaults to OpenAI's hosted API. |
68
+
69
+ Hosted OpenAI:
70
+
71
+ ```
72
+ export LEX_MODEL=gpt-4o-mini
73
+ export LEX_API_KEY=sk-...
74
+ ```
75
+
76
+ A local Ollama server:
77
+
78
+ ```
79
+ export LEX_MODEL=llama3.1
80
+ export LEX_API_KEY=ollama
81
+ export LEX_BASE_URL=http://localhost:11434/v1
82
+ ```
83
+
84
+ `lex` also tells the model which shell you're in, so the command it writes
85
+ matches your shell's syntax. Run `lex shell` to see what it detected.
86
+
87
+ > **Clipboard on Linux.** Copying first tries your system clipboard via
88
+ > [pyperclip](https://pypi.org/project/pyperclip/), which on Linux needs one of
89
+ > `xclip`, `xsel`, or `wl-clipboard` installed. Without one, `lex` falls back
90
+ > to OSC 52 — asking your terminal itself to set the clipboard, no install
91
+ > needed — which modern terminals
92
+ > (VS Code, kitty, alacritty, WezTerm, iTerm2, foot, …) support, even over
93
+ > SSH. If neither route works, the copy is skipped quietly.
94
+
95
+ ## Usage
96
+
97
+ Call `lex` with a description of what you want to do, in quotes:
98
+
99
+ ```
100
+ $ lex "find every python file changed in the last week"
101
+ ```
102
+
103
+ `lex` puts its best guess on your prompt:
104
+
105
+ ```
106
+ $ find . -name '*.py' -mtime -7
107
+ ```
108
+
109
+ From there it's your command like any other — run it, tweak it, or clear the line and try a different description.
110
+
111
+ More examples:
112
+
113
+ ```
114
+ $ lex "undo my last git commit but keep the changes"
115
+ $ git reset --soft HEAD~1
116
+
117
+ $ lex "compress this folder into a tar.gz"
118
+ $ tar -czf folder.tar.gz folder/
119
+
120
+ $ lex "show what's listening on port 8080"
121
+ $ lsof -i :8080
122
+ ```
123
+
124
+ ## Why you're always safe
125
+
126
+ `lex` only ever *writes* a command to your shell. It doesn't run it, and it can't run it — executing is up to you. So if a suggestion looks wrong, you'll see it before anything happens.
@@ -0,0 +1,53 @@
1
+ # ADR 0001 — How the generated command reaches the command line
2
+
3
+ Status: accepted
4
+ Date: 2026-08-26
5
+
6
+ ## Context
7
+
8
+ lex's core promise is that the generated command lands *on your command line*,
9
+ pre-typed and ready to run, without lex executing anything. The spec
10
+ (`docs/specs/command-agent.md`) names the `TIOCSTI` ioctl as the mechanism:
11
+ it pushes bytes into the controlling terminal's input queue, so the shell
12
+ reads them as if they had been typed.
13
+
14
+ `TIOCSTI` was a long-standing local privilege-escalation vector (any process
15
+ sharing a TTY can inject input into another's shell). Linux 6.2 added the
16
+ `dev.tty.legacy_tiocsti` sysctl and distributions ship it **off** by default.
17
+ On the current development machine (kernel 7.0):
18
+
19
+ $ sysctl dev.tty.legacy_tiocsti
20
+ dev.tty.legacy_tiocsti = 0
21
+
22
+ With that setting the ioctl fails with `EPERM` for every caller. So on a
23
+ default modern Linux desktop, injection will **never** succeed — the fallback
24
+ path is the path users actually get.
25
+
26
+ ## Decision
27
+
28
+ Keep `TIOCSTI` as the injection mechanism, isolated in
29
+ `lex_the_hacker/buffer.py`, and treat the stdout fallback (`command: <cmd>`)
30
+ as a first-class, expected outcome rather than an error case:
31
+
32
+ - `buffer.put()` attempts injection and returns whether it succeeded; every
33
+ failure mode (no controlling TTY, missing `termios`/`fcntl`, missing
34
+ `TIOCSTI` constant, `OSError` from the ioctl) falls back to printing.
35
+ - No warning or error is emitted on fallback. Printing the command is a
36
+ useful, complete outcome on its own — the user can copy or pipe it.
37
+ - lex still never executes the command; injection puts text on the line, it
38
+ does not press Enter.
39
+
40
+ ## Consequences
41
+
42
+ - On hardened/modern kernels lex prints the command instead of typing it.
43
+ That is correct behaviour, not a bug, and tests assert the fallback works.
44
+ - Users who want real injection can opt in with
45
+ `sudo sysctl -w dev.tty.legacy_tiocsti=1`, with the understanding that they
46
+ are re-enabling a known privilege-escalation primitive. We do not
47
+ recommend it in the README.
48
+ - The durable fix is shell integration rather than a kernel ioctl — a `zsh`
49
+ widget using `print -z`, a bash `READLINE_LINE` binding, or a shell
50
+ function wrapper that runs `lex` and feeds the result back. The existing
51
+ `lex shell` command (shell name + config file path) is deliberately the
52
+ groundwork for installing such a hook. That is future work, out of scope
53
+ for this spec.
@@ -0,0 +1,130 @@
1
+ # Spec: LLM command-generation agent
2
+
3
+ ## What to build
4
+
5
+ Implement the AI-command-generation behavior the README already promises but
6
+ that doesn't exist yet (per `CLAUDE.md`: "the AI-command-generation behavior
7
+ described in the README is not implemented yet"). Concretely:
8
+
9
+ 1. **A [pydantic-ai](https://ai.pydantic.dev/) agent** (e.g.
10
+ `lex_the_hacker/agent.py`) that talks to an OpenAI-compatible chat
11
+ completions API. "OpenAI-compatible" is the key property — it's what lets
12
+ the same code talk to real OpenAI *or* a local Ollama server (Ollama
13
+ serves an OpenAI-compatible endpoint), without provider-specific branching.
14
+
15
+ - Configure the agent's model/provider from environment variables, not
16
+ hardcoded values:
17
+ - `LEX_MODEL` — the model name to request (e.g. `gpt-4o-mini`,
18
+ `llama3.1`). Required.
19
+ - `LEX_API_KEY` — API key sent to the provider. Required for real
20
+ OpenAI; for Ollama this can be any non-empty placeholder value since
21
+ Ollama doesn't validate it, but the underlying OpenAI client still
22
+ requires the field to be set.
23
+ - `LEX_BASE_URL` — base URL of the OpenAI-compatible endpoint.
24
+ Optional; when unset, use the provider's default (OpenAI's hosted
25
+ API). Set to something like `http://localhost:11434/v1` to target a
26
+ local Ollama instance instead.
27
+ - If a required env var is missing when the agent is invoked, fail with a
28
+ clear, user-facing message (no stack trace) rather than letting the
29
+ underlying HTTP client raise.
30
+
31
+ 2. **A two-step gate → generate flow**, run for each user request:
32
+ - **Gate**: decide whether the request plausibly describes something
33
+ achievable as a single shell command (e.g. "find every python file
34
+ changed in the last week" passes; "write me a poem" or "what's the
35
+ weather" does not).
36
+ - **Generate**: if the gate passes, produce the shell command text for
37
+ the request. If the gate fails, produce a short message telling the
38
+ user lex can't help with that — and do not attempt generation or touch
39
+ the buffer.
40
+ - The gate and generate steps should be distinguishable in the code (not
41
+ necessarily two separate LLM calls if one call can reliably do both,
42
+ but the pass/fail decision and the "can't help" message must be
43
+ reachable and testable independently of whether generation succeeds).
44
+
45
+ 3. **Buffer output**: when generation succeeds, the resulting command must
46
+ land on the user's shell command line, ready to run but not executed —
47
+ this is the tool's existing core promise ("lex drops the command straight
48
+ onto your command line ... it never executes anything itself").
49
+ - Implement this as a small, isolated function (e.g.
50
+ `lex_the_hacker/buffer.py`) that injects the command text into the
51
+ current terminal's input queue (on Linux, the standard mechanism for
52
+ this is the `TIOCSTI` ioctl) so it appears pre-typed at the next
53
+ prompt.
54
+ - If buffer injection isn't possible — no controlling TTY, running
55
+ non-interactively, unsupported platform, or the ioctl is
56
+ rejected/unsupported on the current system — fall back to printing the
57
+ command to stdout with a clear label (e.g. `command: <cmd>`) instead of
58
+ crashing.
59
+
60
+ 4. **CLI wiring**: invoking `lex` with a free-text description and no
61
+ subcommand (e.g. `lex "search the dir for 'xyz'"`) runs the flow above.
62
+ `lex shell`, `lex --version`, and `lex --help` must keep working exactly
63
+ as they do today.
64
+
65
+ 5. **README updates**: document `LEX_MODEL`, `LEX_API_KEY`, and
66
+ `LEX_BASE_URL` — what each does, which are required, and example values
67
+ for both hosted OpenAI and a local Ollama server — so a new user can
68
+ actually connect the tool to a model.
69
+
70
+ ## Acceptance criteria
71
+
72
+ - `pydantic-ai` (or the appropriate `pydantic-ai-slim[...]` extra) is added
73
+ via `uv add` and appears in `pyproject.toml` / `uv.lock`.
74
+ - The agent module builds its model/provider from `LEX_MODEL`,
75
+ `LEX_API_KEY`, and `LEX_BASE_URL` at call time (not import time), so tests
76
+ can set/unset them per-test.
77
+ - Missing `LEX_MODEL` or `LEX_API_KEY` produces a clear `click.ClickException`
78
+ (or equivalent user-facing error) — no unhandled traceback.
79
+ - The gate correctly distinguishes at least a few representative
80
+ command-shaped requests from non-command-shaped requests in tests, using a
81
+ mocked/stubbed model (e.g. pydantic-ai's `TestModel`/`FunctionModel`, or an
82
+ injected fake) — **no real network calls in the test suite**.
83
+ - When the gate rejects a request, the user sees a "can't help with that"
84
+ style message and the buffer-write function is never called.
85
+ - When the gate accepts a request, the generated command string is passed to
86
+ the buffer-write function.
87
+ - The buffer-write function is isolated enough to be mocked in tests (CI has
88
+ no real interactive TTY) and has an explicit, tested fallback path (prints
89
+ to stdout) for when injection isn't possible.
90
+ - Running `lex "<description>"` end-to-end against a real local Ollama
91
+ server (`LEX_BASE_URL=http://localhost:11434/v1`, `LEX_MODEL=<a locally
92
+ pulled model>`, `LEX_API_KEY=ollama`) produces a command on the line, and
93
+ running it against a request lex can't help with prints the "can't help"
94
+ message — both verified manually since this depends on a live model.
95
+ - `lex shell`, `lex --version`, and `lex --help` still work and existing
96
+ tests for them still pass.
97
+ - README documents all three env vars, marks which are required vs.
98
+ optional, and gives one example configuration for OpenAI and one for
99
+ Ollama.
100
+ - `uv run pytest` passes, including new tests for the gate, the generate
101
+ path, the buffer fallback, and the missing-env-var error path.
102
+
103
+ ## Out of scope
104
+
105
+ - Any change to the safety model of the tool — lex still never executes a
106
+ generated command itself; no new execution path is being added.
107
+ - Multi-turn conversation, follow-up refinement, or conversation history.
108
+ - Buffer injection on Windows or non-TTY-based terminals — Unix-like TTYs
109
+ only (Linux is the primary target; best-effort elsewhere is fine but not
110
+ required).
111
+ - Installing, pulling, or managing local Ollama models — the agent only
112
+ connects to an already-running endpoint.
113
+ - Prompt engineering polish beyond a reasonable first pass (few-shot tuning,
114
+ chain-of-thought, etc. can follow later based on real usage).
115
+ - Command preview/explanation UI beyond what's already described in the
116
+ README.
117
+ - Shell-specific command dialects (e.g. tailoring output to the shell
118
+ detected by the existing `lex shell` command) — that integration can come
119
+ later; this spec generates a generic POSIX-shell command.
120
+
121
+ ## Sequencing
122
+
123
+ One cohesive deliverable — the gate, generation, buffer output, and CLI
124
+ entry point only make sense together as a single working feature — but
125
+ build in this order to keep it reviewable:
126
+
127
+ 1. Env var configuration + the pydantic-ai agent (gate and generate).
128
+ 2. The buffer-injection module with its stdout fallback.
129
+ 3. CLI wiring of the two above into `lex "<description>"`, plus the README
130
+ env var documentation.
@@ -0,0 +1,80 @@
1
+ # Spec: Shell detection + `lex shell` command
2
+
3
+ ## What to build
4
+
5
+ `lex` should be able to detect which shell it's running under (bash, zsh,
6
+ fish, etc.) so that future command-suggestion features can tailor output to
7
+ the user's actual shell. This spec covers the detection capability itself
8
+ plus a new `lex shell` command that surfaces what was detected, for the user
9
+ and for debugging.
10
+
11
+ 1. **Shell detection module** (`lex_the_hacker/shell.py` or similar):
12
+ - Use the [`shellingham`](https://pypi.org/project/shellingham/) library
13
+ (the de facto standard for this — used by pip, poetry, pipx, etc.) to
14
+ detect the running shell's name and executable path. Add it as a
15
+ dependency via `uv add shellingham`.
16
+ - `shellingham.detect_shell()` returns `(name, path)` (e.g.
17
+ `("zsh", "/bin/zsh")`) but raises `shellingham.ShellDetectionFailed` if
18
+ it can't determine the shell (e.g. non-interactive/unknown parent
19
+ process). Handle that case explicitly rather than letting it propagate.
20
+ - Given a detected shell name, resolve its primary config/rc file path
21
+ using `$HOME` and standard per-shell conventions:
22
+ - `bash` → `~/.bashrc`
23
+ - `zsh` → `~/.zshrc`
24
+ - `fish` → `~/.config/fish/config.fish`
25
+ - other known shells: use reasonable, documented defaults (e.g. `ksh` →
26
+ `~/.kshrc`, `csh`/`tcsh` → `~/.cshrc`)
27
+ - unrecognized shell name → no config path (`None`), not an error
28
+ - This module should expose a small, testable function/data structure
29
+ (e.g. a `ShellInfo` dataclass with `name`, `path`, `config_path` fields)
30
+ rather than printing directly, so `lex shell` and any future callers
31
+ can reuse it.
32
+
33
+ 2. **`lex shell` command**:
34
+ - Add as a new Click command in `lex_the_hacker/cli.py` (or a subcommand
35
+ module if that becomes unwieldy — for this scope, adding directly to
36
+ `cli.py` is fine).
37
+ - On success, prints the detected shell name, its executable path, and
38
+ its config file path (or a note that the config file convention is
39
+ unknown for that shell).
40
+ - On detection failure, prints a clear message that the shell could not
41
+ be detected (not a stack trace) and exits non-zero.
42
+
43
+ ## Acceptance criteria
44
+
45
+ - `uv add shellingham` has been run and `shellingham` appears in
46
+ `pyproject.toml` dependencies / `uv.lock`.
47
+ - A shell-detection function exists, is unit-testable without needing an
48
+ actual shell subprocess (i.e. `shellingham.detect_shell` is mockable via
49
+ `unittest.mock.patch` or equivalent), and returns shell name, executable
50
+ path, and resolved config file path.
51
+ - Config file resolution covers at minimum: bash, zsh, fish. Falls back
52
+ gracefully (no exception) for any shell name not in the known list.
53
+ - Running `lex shell` in a normal terminal session prints the detected shell
54
+ name, path, and config file path, and exits 0.
55
+ - When detection fails (simulated via mocking `shellingham.detect_shell` to
56
+ raise `ShellDetectionFailed`), `lex shell` prints a user-friendly error
57
+ message and exits with a non-zero status code — no unhandled traceback.
58
+ - `lex --help` lists `shell` as an available command.
59
+ - New tests in `tests/` cover: successful detection + config resolution for
60
+ at least bash/zsh/fish, an unrecognized-shell case (config path is
61
+ `None`/absent but no crash), and the detection-failure path for the CLI
62
+ command. All existing and new tests pass under `uv run pytest`.
63
+
64
+ ## Out of scope
65
+
66
+ - Actually using the detected shell to generate or format AI-suggested
67
+ commands (the README's broader AI-command-generation feature) — this spec
68
+ only covers detection and the inspection command.
69
+ - Shell config *editing* (e.g. auto-appending an alias or PATH entry to the
70
+ detected rc file) — detection and reporting only.
71
+ - Supporting shells beyond common Unix shells (no Windows `cmd.exe`/
72
+ PowerShell-specific config resolution required, though `shellingham` may
73
+ detect them by name).
74
+ - Caching or persisting the detected shell across invocations.
75
+
76
+ ## Sequencing
77
+
78
+ Single cohesive unit of work: the detection module is a prerequisite for the
79
+ command, but both are small enough, and tightly coupled enough, to implement
80
+ and review together as one deliverable.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"