how-cli 0.2.0__tar.gz → 0.3.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.
- how_cli-0.3.0/PKG-INFO +209 -0
- how_cli-0.3.0/README.md +176 -0
- how_cli-0.3.0/how/__init__.py +3 -0
- how_cli-0.3.0/how/actions.py +182 -0
- how_cli-0.3.0/how/core/__init__.py +6 -0
- how_cli-0.3.0/how/core/chains.py +18 -0
- how_cli-0.3.0/how/core/config.py +86 -0
- how_cli-0.3.0/how/core/context.py +85 -0
- how_cli-0.3.0/how/core/exceptions.py +6 -0
- how_cli-0.3.0/how/core/history.py +93 -0
- how_cli-0.3.0/how/core/llm.py +67 -0
- {how_cli-0.2.0 → how_cli-0.3.0}/how/core/parser.py +3 -2
- {how_cli-0.2.0 → how_cli-0.3.0}/how/core/prompts.py +15 -6
- how_cli-0.3.0/how/core/providers.py +110 -0
- how_cli-0.3.0/how/core/safety.py +88 -0
- how_cli-0.3.0/how/core/schema.py +11 -0
- how_cli-0.3.0/how/core/template.py +23 -0
- how_cli-0.3.0/how/formatting.py +71 -0
- how_cli-0.3.0/how/how.py +301 -0
- how_cli-0.3.0/how/infer.py +55 -0
- how_cli-0.3.0/how_cli.egg-info/PKG-INFO +209 -0
- {how_cli-0.2.0 → how_cli-0.3.0}/how_cli.egg-info/SOURCES.txt +12 -1
- {how_cli-0.2.0 → how_cli-0.3.0}/how_cli.egg-info/requires.txt +2 -0
- {how_cli-0.2.0 → how_cli-0.3.0}/requirements.txt +3 -1
- {how_cli-0.2.0 → how_cli-0.3.0}/setup.py +5 -4
- how_cli-0.3.0/tests/test_actions.py +66 -0
- how_cli-0.3.0/tests/test_context.py +64 -0
- how_cli-0.3.0/tests/test_core.py +77 -0
- how_cli-0.3.0/tests/test_fix.py +45 -0
- how_cli-0.3.0/tests/test_ollama.py +68 -0
- how_cli-0.3.0/tests/test_safety.py +67 -0
- how_cli-0.2.0/PKG-INFO +0 -116
- how_cli-0.2.0/README.md +0 -96
- how_cli-0.2.0/how/__init__.py +0 -1
- how_cli-0.2.0/how/core/__init__.py +0 -6
- how_cli-0.2.0/how/core/chains.py +0 -4
- how_cli-0.2.0/how/core/config.py +0 -50
- how_cli-0.2.0/how/core/llm.py +0 -9
- how_cli-0.2.0/how/core/providers.py +0 -17
- how_cli-0.2.0/how/core/schema.py +0 -6
- how_cli-0.2.0/how/core/template.py +0 -19
- how_cli-0.2.0/how/formatting.py +0 -36
- how_cli-0.2.0/how/how.py +0 -82
- how_cli-0.2.0/how/infer.py +0 -28
- how_cli-0.2.0/how_cli.egg-info/PKG-INFO +0 -116
- {how_cli-0.2.0 → how_cli-0.3.0}/LICENSE +0 -0
- {how_cli-0.2.0 → how_cli-0.3.0}/MANIFEST.in +0 -0
- {how_cli-0.2.0 → how_cli-0.3.0}/how/__main__.py +0 -0
- {how_cli-0.2.0 → how_cli-0.3.0}/how_cli.egg-info/dependency_links.txt +0 -0
- {how_cli-0.2.0 → how_cli-0.3.0}/how_cli.egg-info/entry_points.txt +0 -0
- {how_cli-0.2.0 → how_cli-0.3.0}/how_cli.egg-info/top_level.txt +0 -0
- {how_cli-0.2.0 → how_cli-0.3.0}/setup.cfg +0 -0
how_cli-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: how-cli
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: An AI-based CLI assistant to help you with command line & shell.
|
|
5
|
+
Home-page: https://github.com/FireHead90544/how-cli
|
|
6
|
+
Author: Rudransh Joshi (FireHead90544)
|
|
7
|
+
Author-email: rudranshjoshi1806@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Issue Tracker, https://github.com/FireHead90544/how-cli/issues
|
|
10
|
+
Platform: any
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: typer
|
|
14
|
+
Requires-Dist: langchain
|
|
15
|
+
Requires-Dist: langchain-google-genai
|
|
16
|
+
Requires-Dist: langchain-google-vertexai
|
|
17
|
+
Requires-Dist: langchain-groq
|
|
18
|
+
Requires-Dist: langchain-openai
|
|
19
|
+
Requires-Dist: langchain-anthropic
|
|
20
|
+
Requires-Dist: langchain-ollama
|
|
21
|
+
Requires-Dist: pyperclip
|
|
22
|
+
Dynamic: author
|
|
23
|
+
Dynamic: author-email
|
|
24
|
+
Dynamic: description
|
|
25
|
+
Dynamic: description-content-type
|
|
26
|
+
Dynamic: home-page
|
|
27
|
+
Dynamic: license
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
Dynamic: platform
|
|
30
|
+
Dynamic: project-url
|
|
31
|
+
Dynamic: requires-dist
|
|
32
|
+
Dynamic: summary
|
|
33
|
+
|
|
34
|
+
# how-cli
|
|
35
|
+
An AI-based CLI assistant to help you with command line & shell.
|
|
36
|
+
|
|
37
|
+
[](https://opensource.org/licenses/MIT)
|
|
38
|
+
|
|
39
|
+
## Demo
|
|
40
|
+
https://github.com/user-attachments/assets/effefe1a-c0ed-4b60-838c-98f992f6c25f
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- ⚡ **Deferred & Lazy LLM Initialization**: Zero heavy imports during `--help` or startup, preventing startup crashes.
|
|
45
|
+
- 🦙 **Local LLM Support (Ollama)**: Run 100% offline with Ollama models (`qwen2.5-coder`, `llama3.2`, etc.) with no API keys required.
|
|
46
|
+
- 🎯 **Intelligent Environment & Project Context**: Automatically detects your active shell, available package managers (`apt`, `brew`, `dnf`, `pacman`, etc.), and project root markers (`package.json`, `Cargo.toml`, `pyproject.toml`, `go.mod`, `Dockerfile`).
|
|
47
|
+
- 🛡️ **Safety Guardrails**: Heuristic pattern detection warns against destructive commands (`rm -rf`, `dd if=`, `mkfs`, `> /dev/sd`, `chmod -R 777`, `git reset --hard`, `git push --force`, fork bombs, curl-pipe-to-shell) and gates execution with an explicit confirmation phrase.
|
|
48
|
+
- 📋 **Interactive Action Menu & Clipboard**:
|
|
49
|
+
- `[E]xecute`: Safely runs generated commands step-by-step, halting on non-zero exits.
|
|
50
|
+
- `[C]opy`: Copies commands directly to your system clipboard via `pyperclip`.
|
|
51
|
+
- `[M]odify`: Lets you edit commands inline before executing.
|
|
52
|
+
- `[A]bort`: Cleanly exits with status 0.
|
|
53
|
+
- 🔧 **`how fix` Diagnostic Command**: Inspects the last failed shell command from history or stderr and diagnoses fixes.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
**1. Using `pip`**
|
|
60
|
+
```bash
|
|
61
|
+
pip install -U how-cli
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**2. Manual Installation**
|
|
65
|
+
```bash
|
|
66
|
+
git clone https://github.com/FireHead90544/how-cli.git && cd how-cli
|
|
67
|
+
python3 -m venv .venv
|
|
68
|
+
source .venv/bin/activate
|
|
69
|
+
pip install -e .
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
```console
|
|
77
|
+
$ how [OPTIONS] COMMAND [ARGS]...
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Options**:
|
|
81
|
+
* `-v, --version`: Shows the version of the application.
|
|
82
|
+
* `--help`: Show help message and exit.
|
|
83
|
+
|
|
84
|
+
**Commands**:
|
|
85
|
+
* `setup`: Sets up the LLM provider, API key, custom model, or local endpoint.
|
|
86
|
+
* `to`: Sends a task description to the LLM and returns the commands.
|
|
87
|
+
* `fix`: Diagnoses the last failed command from shell history or stderr.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## `how setup`
|
|
92
|
+
|
|
93
|
+
Configure your preferred LLM provider. Supports both cloud providers (OpenAI, Anthropic, Google, Groq) and local models via Ollama.
|
|
94
|
+
|
|
95
|
+
**Interactive Mode**:
|
|
96
|
+
```bash
|
|
97
|
+
how setup
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Non-Interactive Mode**:
|
|
101
|
+
```bash
|
|
102
|
+
# Using local Ollama (no API key needed!)
|
|
103
|
+
how setup --no-interactive --provider Ollama --model qwen2.5-coder:latest --endpoint http://localhost:11434
|
|
104
|
+
|
|
105
|
+
# Using OpenAI
|
|
106
|
+
how setup --no-interactive --provider OpenAI --api-key "sk-..." --model gpt-4o
|
|
107
|
+
|
|
108
|
+
# Using Groq
|
|
109
|
+
how setup --no-interactive --provider GroqLLaMa --api-key "gsk_..."
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## `how to`
|
|
115
|
+
|
|
116
|
+
Translates a natural language query into concrete shell commands tailored to your system and project.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
how to "find all files larger than 100MB"
|
|
120
|
+
how to "install dependencies"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
After commands are generated, the interactive action menu appears:
|
|
124
|
+
```text
|
|
125
|
+
Options: [E]xecute | [C]opy | [M]odify | [A]bort
|
|
126
|
+
Select an action [e/c/m/a/E/C/M/A] (a):
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Safety Guardrails
|
|
130
|
+
When a destructive command is detected (e.g. `rm -rf`, `git push --force`), `how` warns you with a prominent alert panel:
|
|
131
|
+
```text
|
|
132
|
+
╭─────────────────────────── SAFETY GUARDRAIL ALERT ───────────────────────────╮
|
|
133
|
+
│ WARNING: Destructive or High-Risk Command(s) Detected! │
|
|
134
|
+
│ • Recursive Force Delete: rm -rf node_modules │
|
|
135
|
+
│ Permanently deletes files and directories recursively without │
|
|
136
|
+
│ confirmation. │
|
|
137
|
+
│ │
|
|
138
|
+
│ To prevent accidental damage, type the exact confirmation phrase: │
|
|
139
|
+
│ I UNDERSTAND THE RISKS │
|
|
140
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
141
|
+
```
|
|
142
|
+
Typing `y` is rejected; you must explicitly enter `I UNDERSTAND THE RISKS` before execution is permitted.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## `how fix`
|
|
147
|
+
|
|
148
|
+
Diagnoses why your last command failed and suggests the fix.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Automatically read the last failed command from ~/.bash_history or ~/.zsh_history:
|
|
152
|
+
how fix
|
|
153
|
+
|
|
154
|
+
# Provide the command and stderr explicitly:
|
|
155
|
+
how fix -c "git push origin main" -e "error: failed to push some refs" -x 1
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Providers
|
|
161
|
+
|
|
162
|
+
| Provider | Default Model | Package | Requires Key | Tested |
|
|
163
|
+
|:--------:|:-------------:|:-------:|:------------:|:------:|
|
|
164
|
+
| **Ollama** (Local) | `qwen2.5-coder:latest` | `langchain-ollama` | ❌ No | ✅ |
|
|
165
|
+
| GoogleGenAI | `gemma-4-31b-it` | `langchain-google-genai` | ✅ Yes | ✅ |
|
|
166
|
+
| GoogleVertexAI | `gemma-4-31b-it` | `langchain-google-vertexai` | ✅ Yes | ❌ |
|
|
167
|
+
| GroqMistralAI | `mixtral-8x7b-32768` | `langchain-groq` | ✅ Yes | ✅ |
|
|
168
|
+
| GroqLLaMa | `qwen/qwen3.8-27b` | `langchain-groq` | ✅ Yes | ✅ |
|
|
169
|
+
| OpenAI | `gpt-4o` | `langchain-openai` | ✅ Yes | ✅ |
|
|
170
|
+
| Anthropic | `claude-3-5-sonnet-20240620` | `langchain-anthropic` | ✅ Yes | ✅ |
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Shell Integration
|
|
175
|
+
|
|
176
|
+
You can bind `how to` directly to a keyboard shortcut (e.g. `Ctrl+G`) in your shell to immediately convert your typed command buffer into an AI prompt:
|
|
177
|
+
|
|
178
|
+
### Zsh (`~/.zshrc`)
|
|
179
|
+
```bash
|
|
180
|
+
how-widget() {
|
|
181
|
+
BUFFER="how to \"$BUFFER\""
|
|
182
|
+
zle accept-line
|
|
183
|
+
}
|
|
184
|
+
zle -N how-widget
|
|
185
|
+
bindkey '^G' how-widget
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Bash (`~/.bashrc`)
|
|
189
|
+
```bash
|
|
190
|
+
how-widget() {
|
|
191
|
+
local cmd="how to \"$READLINE_LINE\""
|
|
192
|
+
READLINE_LINE=""
|
|
193
|
+
eval "$cmd"
|
|
194
|
+
}
|
|
195
|
+
bind -x '"\C-g": how-widget'
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
`how-cli` is licensed under the MIT License. See [LICENSE](LICENSE) for details.
|
|
202
|
+
|
|
203
|
+
## Honourable Mentions
|
|
204
|
+
This project is inspired by [kynnyhsap's](https://github.com/kynnyhsap) [how](https://github.com/kynnyhsap/how).
|
|
205
|
+
|
|
206
|
+
## Maintainers
|
|
207
|
+
|
|
208
|
+
- [@FireHead90544](https://github.com/FireHead90544) — Creator & Primary Maintainer
|
|
209
|
+
- [@ashishsinghbora](https://github.com/ashishsinghbora) — Contributor
|
how_cli-0.3.0/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# how-cli
|
|
2
|
+
An AI-based CLI assistant to help you with command line & shell.
|
|
3
|
+
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
## Demo
|
|
7
|
+
https://github.com/user-attachments/assets/effefe1a-c0ed-4b60-838c-98f992f6c25f
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- ⚡ **Deferred & Lazy LLM Initialization**: Zero heavy imports during `--help` or startup, preventing startup crashes.
|
|
12
|
+
- 🦙 **Local LLM Support (Ollama)**: Run 100% offline with Ollama models (`qwen2.5-coder`, `llama3.2`, etc.) with no API keys required.
|
|
13
|
+
- 🎯 **Intelligent Environment & Project Context**: Automatically detects your active shell, available package managers (`apt`, `brew`, `dnf`, `pacman`, etc.), and project root markers (`package.json`, `Cargo.toml`, `pyproject.toml`, `go.mod`, `Dockerfile`).
|
|
14
|
+
- 🛡️ **Safety Guardrails**: Heuristic pattern detection warns against destructive commands (`rm -rf`, `dd if=`, `mkfs`, `> /dev/sd`, `chmod -R 777`, `git reset --hard`, `git push --force`, fork bombs, curl-pipe-to-shell) and gates execution with an explicit confirmation phrase.
|
|
15
|
+
- 📋 **Interactive Action Menu & Clipboard**:
|
|
16
|
+
- `[E]xecute`: Safely runs generated commands step-by-step, halting on non-zero exits.
|
|
17
|
+
- `[C]opy`: Copies commands directly to your system clipboard via `pyperclip`.
|
|
18
|
+
- `[M]odify`: Lets you edit commands inline before executing.
|
|
19
|
+
- `[A]bort`: Cleanly exits with status 0.
|
|
20
|
+
- 🔧 **`how fix` Diagnostic Command**: Inspects the last failed shell command from history or stderr and diagnoses fixes.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
**1. Using `pip`**
|
|
27
|
+
```bash
|
|
28
|
+
pip install -U how-cli
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**2. Manual Installation**
|
|
32
|
+
```bash
|
|
33
|
+
git clone https://github.com/FireHead90544/how-cli.git && cd how-cli
|
|
34
|
+
python3 -m venv .venv
|
|
35
|
+
source .venv/bin/activate
|
|
36
|
+
pip install -e .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```console
|
|
44
|
+
$ how [OPTIONS] COMMAND [ARGS]...
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Options**:
|
|
48
|
+
* `-v, --version`: Shows the version of the application.
|
|
49
|
+
* `--help`: Show help message and exit.
|
|
50
|
+
|
|
51
|
+
**Commands**:
|
|
52
|
+
* `setup`: Sets up the LLM provider, API key, custom model, or local endpoint.
|
|
53
|
+
* `to`: Sends a task description to the LLM and returns the commands.
|
|
54
|
+
* `fix`: Diagnoses the last failed command from shell history or stderr.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## `how setup`
|
|
59
|
+
|
|
60
|
+
Configure your preferred LLM provider. Supports both cloud providers (OpenAI, Anthropic, Google, Groq) and local models via Ollama.
|
|
61
|
+
|
|
62
|
+
**Interactive Mode**:
|
|
63
|
+
```bash
|
|
64
|
+
how setup
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Non-Interactive Mode**:
|
|
68
|
+
```bash
|
|
69
|
+
# Using local Ollama (no API key needed!)
|
|
70
|
+
how setup --no-interactive --provider Ollama --model qwen2.5-coder:latest --endpoint http://localhost:11434
|
|
71
|
+
|
|
72
|
+
# Using OpenAI
|
|
73
|
+
how setup --no-interactive --provider OpenAI --api-key "sk-..." --model gpt-4o
|
|
74
|
+
|
|
75
|
+
# Using Groq
|
|
76
|
+
how setup --no-interactive --provider GroqLLaMa --api-key "gsk_..."
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## `how to`
|
|
82
|
+
|
|
83
|
+
Translates a natural language query into concrete shell commands tailored to your system and project.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
how to "find all files larger than 100MB"
|
|
87
|
+
how to "install dependencies"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
After commands are generated, the interactive action menu appears:
|
|
91
|
+
```text
|
|
92
|
+
Options: [E]xecute | [C]opy | [M]odify | [A]bort
|
|
93
|
+
Select an action [e/c/m/a/E/C/M/A] (a):
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Safety Guardrails
|
|
97
|
+
When a destructive command is detected (e.g. `rm -rf`, `git push --force`), `how` warns you with a prominent alert panel:
|
|
98
|
+
```text
|
|
99
|
+
╭─────────────────────────── SAFETY GUARDRAIL ALERT ───────────────────────────╮
|
|
100
|
+
│ WARNING: Destructive or High-Risk Command(s) Detected! │
|
|
101
|
+
│ • Recursive Force Delete: rm -rf node_modules │
|
|
102
|
+
│ Permanently deletes files and directories recursively without │
|
|
103
|
+
│ confirmation. │
|
|
104
|
+
│ │
|
|
105
|
+
│ To prevent accidental damage, type the exact confirmation phrase: │
|
|
106
|
+
│ I UNDERSTAND THE RISKS │
|
|
107
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
108
|
+
```
|
|
109
|
+
Typing `y` is rejected; you must explicitly enter `I UNDERSTAND THE RISKS` before execution is permitted.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## `how fix`
|
|
114
|
+
|
|
115
|
+
Diagnoses why your last command failed and suggests the fix.
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Automatically read the last failed command from ~/.bash_history or ~/.zsh_history:
|
|
119
|
+
how fix
|
|
120
|
+
|
|
121
|
+
# Provide the command and stderr explicitly:
|
|
122
|
+
how fix -c "git push origin main" -e "error: failed to push some refs" -x 1
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Providers
|
|
128
|
+
|
|
129
|
+
| Provider | Default Model | Package | Requires Key | Tested |
|
|
130
|
+
|:--------:|:-------------:|:-------:|:------------:|:------:|
|
|
131
|
+
| **Ollama** (Local) | `qwen2.5-coder:latest` | `langchain-ollama` | ❌ No | ✅ |
|
|
132
|
+
| GoogleGenAI | `gemma-4-31b-it` | `langchain-google-genai` | ✅ Yes | ✅ |
|
|
133
|
+
| GoogleVertexAI | `gemma-4-31b-it` | `langchain-google-vertexai` | ✅ Yes | ❌ |
|
|
134
|
+
| GroqMistralAI | `mixtral-8x7b-32768` | `langchain-groq` | ✅ Yes | ✅ |
|
|
135
|
+
| GroqLLaMa | `qwen/qwen3.8-27b` | `langchain-groq` | ✅ Yes | ✅ |
|
|
136
|
+
| OpenAI | `gpt-4o` | `langchain-openai` | ✅ Yes | ✅ |
|
|
137
|
+
| Anthropic | `claude-3-5-sonnet-20240620` | `langchain-anthropic` | ✅ Yes | ✅ |
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Shell Integration
|
|
142
|
+
|
|
143
|
+
You can bind `how to` directly to a keyboard shortcut (e.g. `Ctrl+G`) in your shell to immediately convert your typed command buffer into an AI prompt:
|
|
144
|
+
|
|
145
|
+
### Zsh (`~/.zshrc`)
|
|
146
|
+
```bash
|
|
147
|
+
how-widget() {
|
|
148
|
+
BUFFER="how to \"$BUFFER\""
|
|
149
|
+
zle accept-line
|
|
150
|
+
}
|
|
151
|
+
zle -N how-widget
|
|
152
|
+
bindkey '^G' how-widget
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Bash (`~/.bashrc`)
|
|
156
|
+
```bash
|
|
157
|
+
how-widget() {
|
|
158
|
+
local cmd="how to \"$READLINE_LINE\""
|
|
159
|
+
READLINE_LINE=""
|
|
160
|
+
eval "$cmd"
|
|
161
|
+
}
|
|
162
|
+
bind -x '"\C-g": how-widget'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
`how-cli` is licensed under the MIT License. See [LICENSE](LICENSE) for details.
|
|
169
|
+
|
|
170
|
+
## Honourable Mentions
|
|
171
|
+
This project is inspired by [kynnyhsap's](https://github.com/kynnyhsap) [how](https://github.com/kynnyhsap/how).
|
|
172
|
+
|
|
173
|
+
## Maintainers
|
|
174
|
+
|
|
175
|
+
- [@FireHead90544](https://github.com/FireHead90544) — Creator & Primary Maintainer
|
|
176
|
+
- [@ashishsinghbora](https://github.com/ashishsinghbora) — Contributor
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.prompt import Prompt
|
|
8
|
+
|
|
9
|
+
from how.core.safety import assess_risk
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
CONFIRMATION_PHRASE = "I UNDERSTAND THE RISKS"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check_safety_gate(commands: list[str]) -> bool:
|
|
17
|
+
"""
|
|
18
|
+
Check if commands contain destructive patterns. If so, display a bold
|
|
19
|
+
warning panel and require an exact confirmation phrase.
|
|
20
|
+
"""
|
|
21
|
+
flags = assess_risk(commands)
|
|
22
|
+
if not flags:
|
|
23
|
+
return True
|
|
24
|
+
|
|
25
|
+
warning_text = "[bold red]WARNING: Destructive or High-Risk Command(s) Detected![/bold red]\n\n"
|
|
26
|
+
for flag in flags:
|
|
27
|
+
warning_text += (
|
|
28
|
+
f"• [bold yellow]{flag.rule_name}[/bold yellow]: [bold white]{flag.command}[/bold white]\n"
|
|
29
|
+
f" {flag.description}\n"
|
|
30
|
+
)
|
|
31
|
+
warning_text += (
|
|
32
|
+
f"\nTo prevent accidental damage, type the exact confirmation phrase:\n"
|
|
33
|
+
f"[bold white]{CONFIRMATION_PHRASE}[/bold white]"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
console.print(
|
|
37
|
+
Panel(
|
|
38
|
+
warning_text,
|
|
39
|
+
title="[bold red]SAFETY GUARDRAIL ALERT[/bold red]",
|
|
40
|
+
border_style="red",
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
user_input = Prompt.ask("Confirmation phrase")
|
|
46
|
+
if user_input.strip() != CONFIRMATION_PHRASE:
|
|
47
|
+
console.print(
|
|
48
|
+
"[yellow]Confirmation phrase mismatch. Execution blocked.[/yellow]"
|
|
49
|
+
)
|
|
50
|
+
return False
|
|
51
|
+
return True
|
|
52
|
+
except KeyboardInterrupt:
|
|
53
|
+
console.print("\n[yellow]Execution blocked.[/yellow]")
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def copy_to_clipboard(commands: list[str]) -> bool:
|
|
58
|
+
"""Copy commands to clipboard via pyperclip."""
|
|
59
|
+
joined = "\n".join(commands)
|
|
60
|
+
try:
|
|
61
|
+
import pyperclip
|
|
62
|
+
|
|
63
|
+
pyperclip.copy(joined)
|
|
64
|
+
console.print("[bold green]✓ Commands copied to clipboard![/bold green]")
|
|
65
|
+
return True
|
|
66
|
+
except Exception as e: # noqa: BLE001
|
|
67
|
+
console.print(
|
|
68
|
+
f"[yellow]! Clipboard copy failed: {e}. "
|
|
69
|
+
"Please ensure xclip, xsel, or wl-clipboard is installed.[/yellow]"
|
|
70
|
+
)
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def execute_commands(commands: list[str]) -> int:
|
|
75
|
+
"""
|
|
76
|
+
Run commands sequentially via subprocess.run(cmd, shell=True)
|
|
77
|
+
after confirmation and safety verification.
|
|
78
|
+
"""
|
|
79
|
+
if not commands:
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
if not check_safety_gate(commands):
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
if not typer.confirm("Are you sure you want to execute these commands?"):
|
|
86
|
+
console.print("[yellow]Execution cancelled.[/yellow]")
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
for idx, cmd in enumerate(commands, start=1):
|
|
90
|
+
console.print(f"\n[bold cyan]({idx}/{len(commands)}) $ {cmd}[/bold cyan]")
|
|
91
|
+
try:
|
|
92
|
+
proc = subprocess.run(cmd, shell=True, check=False)
|
|
93
|
+
if proc.returncode != 0:
|
|
94
|
+
console.print(
|
|
95
|
+
f"[bold red]Command failed with exit code {proc.returncode}[/bold red]"
|
|
96
|
+
)
|
|
97
|
+
if idx < len(commands):
|
|
98
|
+
continue_run = typer.confirm(
|
|
99
|
+
"Do you want to continue running the remaining commands?",
|
|
100
|
+
default=False,
|
|
101
|
+
)
|
|
102
|
+
if not continue_run:
|
|
103
|
+
console.print("[yellow]Execution stopped.[/yellow]")
|
|
104
|
+
return proc.returncode
|
|
105
|
+
else:
|
|
106
|
+
return proc.returncode
|
|
107
|
+
else:
|
|
108
|
+
console.print("[green]✓ Success (exit code 0)[/green]")
|
|
109
|
+
except KeyboardInterrupt:
|
|
110
|
+
console.print("\n[yellow]Execution interrupted by user.[/yellow]")
|
|
111
|
+
return 130
|
|
112
|
+
except OSError as e:
|
|
113
|
+
console.print(f"[bold red]Error executing command: {e}[/bold red]")
|
|
114
|
+
return 1
|
|
115
|
+
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def modify_commands(commands: list[str]) -> list[str]:
|
|
120
|
+
"""Allow inline editing of the commands."""
|
|
121
|
+
joined = "\n".join(commands)
|
|
122
|
+
console.print("[cyan]Modify the command below:[/cyan]")
|
|
123
|
+
try:
|
|
124
|
+
import readline
|
|
125
|
+
|
|
126
|
+
def hook() -> None:
|
|
127
|
+
readline.insert_text(joined)
|
|
128
|
+
readline.set_startup_hook()
|
|
129
|
+
|
|
130
|
+
readline.set_startup_hook(hook)
|
|
131
|
+
edited = input("> ")
|
|
132
|
+
except Exception: # noqa: BLE001
|
|
133
|
+
edited = Prompt.ask("Edit command", default=joined)
|
|
134
|
+
finally:
|
|
135
|
+
with contextlib.suppress(Exception):
|
|
136
|
+
import readline
|
|
137
|
+
|
|
138
|
+
readline.set_startup_hook(None)
|
|
139
|
+
|
|
140
|
+
new_cmds = [line.strip() for line in edited.splitlines() if line.strip()]
|
|
141
|
+
if new_cmds:
|
|
142
|
+
return new_cmds
|
|
143
|
+
return commands
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def interactive_action_menu(commands: list[str]) -> None:
|
|
147
|
+
"""Interactive action menu offering Execute, Copy, Modify, and Abort."""
|
|
148
|
+
current_commands = list(commands)
|
|
149
|
+
if not current_commands:
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
while True:
|
|
153
|
+
try:
|
|
154
|
+
console.print(
|
|
155
|
+
"\n[bold]Options:[/bold] "
|
|
156
|
+
"[bold green][E]xecute[/bold green] | "
|
|
157
|
+
"[bold cyan][C]opy[/bold cyan] | "
|
|
158
|
+
"[bold yellow][M]odify[/bold yellow] | "
|
|
159
|
+
"[bold red][A]bort[/bold red]"
|
|
160
|
+
)
|
|
161
|
+
choice = Prompt.ask(
|
|
162
|
+
"Select an action",
|
|
163
|
+
choices=["e", "c", "m", "a", "E", "C", "M", "A"],
|
|
164
|
+
default="a",
|
|
165
|
+
).lower()
|
|
166
|
+
|
|
167
|
+
if choice == "e":
|
|
168
|
+
execute_commands(current_commands)
|
|
169
|
+
break
|
|
170
|
+
elif choice == "c":
|
|
171
|
+
copy_to_clipboard(current_commands)
|
|
172
|
+
elif choice == "m":
|
|
173
|
+
current_commands = modify_commands(current_commands)
|
|
174
|
+
console.print(
|
|
175
|
+
f"[green]Updated command(s):[/green] {', '.join(current_commands)}"
|
|
176
|
+
)
|
|
177
|
+
elif choice == "a":
|
|
178
|
+
console.print("[yellow]Aborted.[/yellow]")
|
|
179
|
+
break
|
|
180
|
+
except KeyboardInterrupt:
|
|
181
|
+
console.print("\n[yellow]Aborted.[/yellow]")
|
|
182
|
+
break
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_chain(llm: Any | None = None) -> Any:
|
|
5
|
+
"""Build and return the LangChain runnable chain using an initialized LLM."""
|
|
6
|
+
from how.core.llm import get_llm
|
|
7
|
+
from how.core.template import PROMPT_TEMPLATE
|
|
8
|
+
|
|
9
|
+
if llm is None:
|
|
10
|
+
llm = get_llm()
|
|
11
|
+
|
|
12
|
+
return PROMPT_TEMPLATE | llm
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def __getattr__(name: str) -> Any:
|
|
16
|
+
if name == "HOW_CLI_CHAIN":
|
|
17
|
+
return get_chain()
|
|
18
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Config:
|
|
7
|
+
"""
|
|
8
|
+
Manages the configuration of the application.
|
|
9
|
+
Config file is stored in ~/.how/config.json by default.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, config_dir: Path | None = None) -> None:
|
|
13
|
+
self.config_dir = config_dir or (Path.home() / ".how")
|
|
14
|
+
self.config_file = self.config_dir / "config.json"
|
|
15
|
+
self.__init_config()
|
|
16
|
+
|
|
17
|
+
def __init_config(self) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Initialize the configuration directory & file if they don't exist.
|
|
20
|
+
"""
|
|
21
|
+
if not self.config_dir.exists():
|
|
22
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
|
|
24
|
+
if not self.config_file.exists():
|
|
25
|
+
with open(self.config_file, "w", encoding="utf-8") as f:
|
|
26
|
+
json.dump(
|
|
27
|
+
{
|
|
28
|
+
"provider": "",
|
|
29
|
+
"api_key": "",
|
|
30
|
+
"model": "",
|
|
31
|
+
"endpoint": "",
|
|
32
|
+
},
|
|
33
|
+
f,
|
|
34
|
+
indent=4,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def setup(
|
|
38
|
+
self,
|
|
39
|
+
provider: str,
|
|
40
|
+
api_key: str = "",
|
|
41
|
+
model: str | None = None,
|
|
42
|
+
endpoint: str | None = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Save the LLM Provider, API Key, and optional custom model/endpoint.
|
|
46
|
+
"""
|
|
47
|
+
data: dict[str, Any] = {
|
|
48
|
+
"provider": provider,
|
|
49
|
+
"api_key": api_key,
|
|
50
|
+
"model": model or "",
|
|
51
|
+
"endpoint": endpoint or "",
|
|
52
|
+
}
|
|
53
|
+
with open(self.config_file, "w", encoding="utf-8") as f:
|
|
54
|
+
json.dump(data, f, indent=4)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def values(self) -> dict[str, Any]:
|
|
58
|
+
"""
|
|
59
|
+
Get the configuration values.
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
with open(self.config_file, "r", encoding="utf-8") as f:
|
|
63
|
+
return json.load(f)
|
|
64
|
+
except (OSError, json.JSONDecodeError):
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
def is_ready(self) -> bool:
|
|
68
|
+
"""
|
|
69
|
+
Check if the configuration file is ready to use.
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
vals = self.values
|
|
73
|
+
provider = vals.get("provider")
|
|
74
|
+
if not provider:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
from how.core.providers import LLM_PROVIDERS
|
|
78
|
+
|
|
79
|
+
provider_info = LLM_PROVIDERS.get(provider)
|
|
80
|
+
if not provider_info:
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
requires_key = provider_info.get("requires_key", True)
|
|
84
|
+
return not (requires_key and not vals.get("api_key"))
|
|
85
|
+
except (KeyError, TypeError, ValueError, ImportError):
|
|
86
|
+
return False
|