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.
- lex_the_hacker-0.1.0/.claude/skills/dev/SKILL.md +42 -0
- lex_the_hacker-0.1.0/.gitignore +10 -0
- lex_the_hacker-0.1.0/.python-version +1 -0
- lex_the_hacker-0.1.0/CLAUDE.md +24 -0
- lex_the_hacker-0.1.0/PKG-INFO +139 -0
- lex_the_hacker-0.1.0/README.md +126 -0
- lex_the_hacker-0.1.0/docs/adr/0001-command-buffer-injection.md +53 -0
- lex_the_hacker-0.1.0/docs/images/lex.jpeg +0 -0
- lex_the_hacker-0.1.0/docs/specs/command-agent.md +130 -0
- lex_the_hacker-0.1.0/docs/specs/detect-shell.md +80 -0
- lex_the_hacker-0.1.0/lex_the_hacker/__init__.py +1 -0
- lex_the_hacker-0.1.0/lex_the_hacker/agent.py +114 -0
- lex_the_hacker-0.1.0/lex_the_hacker/buffer.py +64 -0
- lex_the_hacker-0.1.0/lex_the_hacker/cli.py +124 -0
- lex_the_hacker-0.1.0/lex_the_hacker/shell.py +44 -0
- lex_the_hacker-0.1.0/pyproject.toml +26 -0
- lex_the_hacker-0.1.0/tests/conftest.py +18 -0
- lex_the_hacker-0.1.0/tests/test_agent.py +144 -0
- lex_the_hacker-0.1.0/tests/test_buffer.py +106 -0
- lex_the_hacker-0.1.0/tests/test_cli.py +210 -0
- lex_the_hacker-0.1.0/tests/test_shell.py +58 -0
- lex_the_hacker-0.1.0/uv.lock +793 -0
|
@@ -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 @@
|
|
|
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
|
+

|
|
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
|
+

|
|
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.
|
|
Binary file
|
|
@@ -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"
|