gpt-command 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.
- gpt_command-0.1.0/PKG-INFO +143 -0
- gpt_command-0.1.0/README.md +133 -0
- gpt_command-0.1.0/pyproject.toml +27 -0
- gpt_command-0.1.0/setup.cfg +4 -0
- gpt_command-0.1.0/src/gpt_command/__init__.py +1 -0
- gpt_command-0.1.0/src/gpt_command/cli.py +226 -0
- gpt_command-0.1.0/src/gpt_command/config.py +76 -0
- gpt_command-0.1.0/src/gpt_command/history.py +53 -0
- gpt_command-0.1.0/src/gpt_command/key_manager.py +80 -0
- gpt_command-0.1.0/src/gpt_command/utils.py +117 -0
- gpt_command-0.1.0/src/gpt_command.egg-info/PKG-INFO +143 -0
- gpt_command-0.1.0/src/gpt_command.egg-info/SOURCES.txt +14 -0
- gpt_command-0.1.0/src/gpt_command.egg-info/dependency_links.txt +1 -0
- gpt_command-0.1.0/src/gpt_command.egg-info/entry_points.txt +3 -0
- gpt_command-0.1.0/src/gpt_command.egg-info/requires.txt +2 -0
- gpt_command-0.1.0/src/gpt_command.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gpt-command
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Ask GPT for Ubuntu/macOS/Linux shell commands from natural language
|
|
5
|
+
Author: Your Name
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: openai>=2.0.0
|
|
9
|
+
Requires-Dist: tqdm>=4.66.0
|
|
10
|
+
|
|
11
|
+
# gpt-command (gptc)
|
|
12
|
+
|
|
13
|
+
> Turn natural language into ready-to-use terminal commands.
|
|
14
|
+
|
|
15
|
+
`gptc` is a lightweight CLI tool that converts plain English (or any language) into executable shell commands for macOS and Linux.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🚀 Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install gpt-command
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
🔑 Setup API Key
|
|
28
|
+
|
|
29
|
+
Run once to store your OpenAI API key securely:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
gptc-key
|
|
33
|
+
```
|
|
34
|
+
Check status:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
gptc-key --status
|
|
38
|
+
```
|
|
39
|
+
Set default model (optional):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
gptc-key --model gpt-4.1
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
💡 Usage
|
|
48
|
+
```bash
|
|
49
|
+
gptc <your question>
|
|
50
|
+
```
|
|
51
|
+
Example
|
|
52
|
+
```bash
|
|
53
|
+
gptc find and delete all txt files in current directory recursively
|
|
54
|
+
```
|
|
55
|
+
Output:
|
|
56
|
+
```bash
|
|
57
|
+
find . -type f -name "*.txt" -delete
|
|
58
|
+
```
|
|
59
|
+
Then it will automatically prefill your terminal input:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
~/current/path$ find . -type f -name "*.txt" -delete
|
|
63
|
+
```
|
|
64
|
+
⚠️ The command is NOT executed automatically.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
⚙️ Options
|
|
69
|
+
|
|
70
|
+
📋 Copy to clipboard
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
gptc --copy compress current folder into tar.gz
|
|
74
|
+
```
|
|
75
|
+
📖 Show explanation
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
gptc --explain find process using port 8000
|
|
79
|
+
```
|
|
80
|
+
▶️ Execute command (with confirmation)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
gptc --run check disk usage
|
|
84
|
+
```
|
|
85
|
+
🧠 Show history
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
gptc --history
|
|
89
|
+
```
|
|
90
|
+
🤖 Specify model
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
gptc --model gpt-4.1 list all running processes
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
🔐 Security
|
|
99
|
+
|
|
100
|
+
- API key is stored locally at:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
~/.config/gptc/config.json
|
|
104
|
+
```
|
|
105
|
+
File permissions are restricted to the user (600)
|
|
106
|
+
Dangerous commands are automatically detected and blocked from execution
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
⚠️ Disclaimer
|
|
111
|
+
- Always review generated commands before running them
|
|
112
|
+
- Some commands may modify or delete system data
|
|
113
|
+
- Use with caution, especially with elevated privileges (sudo)
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
🖥️ Supported Platforms
|
|
118
|
+
|
|
119
|
+
- macOS
|
|
120
|
+
- Linux (Ubuntu, etc.)
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
✨ Features
|
|
125
|
+
- Natural language → shell command
|
|
126
|
+
- Auto-prefilled terminal input (no copy-paste needed)
|
|
127
|
+
- Optional execution with confirmation
|
|
128
|
+
- Clipboard copy support
|
|
129
|
+
- Command explanation
|
|
130
|
+
- History tracking
|
|
131
|
+
- Local API key management (no environment variable required)
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
📌 Summary
|
|
136
|
+
|
|
137
|
+
> Stop Googling terminal commands. Just ask.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
📄 License
|
|
142
|
+
|
|
143
|
+
MIT License
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# gpt-command (gptc)
|
|
2
|
+
|
|
3
|
+
> Turn natural language into ready-to-use terminal commands.
|
|
4
|
+
|
|
5
|
+
`gptc` is a lightweight CLI tool that converts plain English (or any language) into executable shell commands for macOS and Linux.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🚀 Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install gpt-command
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
🔑 Setup API Key
|
|
18
|
+
|
|
19
|
+
Run once to store your OpenAI API key securely:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gptc-key
|
|
23
|
+
```
|
|
24
|
+
Check status:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
gptc-key --status
|
|
28
|
+
```
|
|
29
|
+
Set default model (optional):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
gptc-key --model gpt-4.1
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
💡 Usage
|
|
38
|
+
```bash
|
|
39
|
+
gptc <your question>
|
|
40
|
+
```
|
|
41
|
+
Example
|
|
42
|
+
```bash
|
|
43
|
+
gptc find and delete all txt files in current directory recursively
|
|
44
|
+
```
|
|
45
|
+
Output:
|
|
46
|
+
```bash
|
|
47
|
+
find . -type f -name "*.txt" -delete
|
|
48
|
+
```
|
|
49
|
+
Then it will automatically prefill your terminal input:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
~/current/path$ find . -type f -name "*.txt" -delete
|
|
53
|
+
```
|
|
54
|
+
⚠️ The command is NOT executed automatically.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
⚙️ Options
|
|
59
|
+
|
|
60
|
+
📋 Copy to clipboard
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
gptc --copy compress current folder into tar.gz
|
|
64
|
+
```
|
|
65
|
+
📖 Show explanation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
gptc --explain find process using port 8000
|
|
69
|
+
```
|
|
70
|
+
▶️ Execute command (with confirmation)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
gptc --run check disk usage
|
|
74
|
+
```
|
|
75
|
+
🧠 Show history
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
gptc --history
|
|
79
|
+
```
|
|
80
|
+
🤖 Specify model
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
gptc --model gpt-4.1 list all running processes
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
🔐 Security
|
|
89
|
+
|
|
90
|
+
- API key is stored locally at:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
~/.config/gptc/config.json
|
|
94
|
+
```
|
|
95
|
+
File permissions are restricted to the user (600)
|
|
96
|
+
Dangerous commands are automatically detected and blocked from execution
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
⚠️ Disclaimer
|
|
101
|
+
- Always review generated commands before running them
|
|
102
|
+
- Some commands may modify or delete system data
|
|
103
|
+
- Use with caution, especially with elevated privileges (sudo)
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
🖥️ Supported Platforms
|
|
108
|
+
|
|
109
|
+
- macOS
|
|
110
|
+
- Linux (Ubuntu, etc.)
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
✨ Features
|
|
115
|
+
- Natural language → shell command
|
|
116
|
+
- Auto-prefilled terminal input (no copy-paste needed)
|
|
117
|
+
- Optional execution with confirmation
|
|
118
|
+
- Clipboard copy support
|
|
119
|
+
- Command explanation
|
|
120
|
+
- History tracking
|
|
121
|
+
- Local API key management (no environment variable required)
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
📌 Summary
|
|
126
|
+
|
|
127
|
+
> Stop Googling terminal commands. Just ask.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
📄 License
|
|
132
|
+
|
|
133
|
+
MIT License
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gpt-command"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Ask GPT for Ubuntu/macOS/Linux shell commands from natural language"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Your Name" }
|
|
13
|
+
]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"openai>=2.0.0",
|
|
16
|
+
"tqdm>=4.66.0"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
gptc = "gpt_command.cli:main"
|
|
21
|
+
gptc-key = "gpt_command.key_manager:main"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
package-dir = {"" = "src"}
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["src"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from openai import OpenAI
|
|
5
|
+
from tqdm import tqdm
|
|
6
|
+
|
|
7
|
+
from .config import get_api_key, get_default_model
|
|
8
|
+
from .history import print_history, save_history_item
|
|
9
|
+
from .utils import (
|
|
10
|
+
copy_to_clipboard,
|
|
11
|
+
extract_command,
|
|
12
|
+
is_dangerous_command,
|
|
13
|
+
prefilled_input,
|
|
14
|
+
run_shell_command,
|
|
15
|
+
yes_no,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
SYSTEM_PROMPT = """You are a Linux/macOS terminal command generator.
|
|
19
|
+
|
|
20
|
+
Follow these rules exactly:
|
|
21
|
+
1. Output only one executable shell command on a single line.
|
|
22
|
+
2. Do not output explanations, code fences, quotes, or extra text.
|
|
23
|
+
3. Prefer commands that work in bash/zsh environments.
|
|
24
|
+
4. If the user's request is risky or destructive, suggest the safest practical command possible.
|
|
25
|
+
5. If multiple steps are needed, combine them into a single one-liner using && when appropriate.
|
|
26
|
+
6. Return only the command string.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
EXPLAIN_PROMPT = """You are a Linux/macOS terminal command explainer.
|
|
30
|
+
Explain the given command in up to 3 short lines:
|
|
31
|
+
1. What it does
|
|
32
|
+
2. What the important options mean
|
|
33
|
+
3. Any caution the user should know
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def ask_model_for_command(question: str, model: str) -> str:
|
|
38
|
+
api_key = get_api_key()
|
|
39
|
+
if not api_key:
|
|
40
|
+
raise RuntimeError(
|
|
41
|
+
"No OpenAI API key found. Run `gptc-key` first to store your API key."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
client = OpenAI(api_key=api_key)
|
|
45
|
+
|
|
46
|
+
with tqdm(total=3, desc="GPT processing", ncols=88) as pbar:
|
|
47
|
+
pbar.set_description("Preparing request")
|
|
48
|
+
pbar.update(1)
|
|
49
|
+
|
|
50
|
+
response = client.responses.create(
|
|
51
|
+
model=model,
|
|
52
|
+
instructions=SYSTEM_PROMPT,
|
|
53
|
+
input=f"Convert this natural language request into a single shell command:\n{question}",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
pbar.set_description("Receiving response")
|
|
57
|
+
pbar.update(1)
|
|
58
|
+
|
|
59
|
+
text = getattr(response, "output_text", "") or ""
|
|
60
|
+
command = extract_command(text)
|
|
61
|
+
|
|
62
|
+
pbar.set_description("Finalizing result")
|
|
63
|
+
pbar.update(1)
|
|
64
|
+
|
|
65
|
+
if not command:
|
|
66
|
+
raise RuntimeError("Failed to extract a command from the model response.")
|
|
67
|
+
|
|
68
|
+
return command
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def ask_model_for_explanation(command: str, model: str) -> str:
|
|
72
|
+
api_key = get_api_key()
|
|
73
|
+
if not api_key:
|
|
74
|
+
raise RuntimeError(
|
|
75
|
+
"No OpenAI API key found. Run `gptc-key` first to store your API key."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
client = OpenAI(api_key=api_key)
|
|
79
|
+
|
|
80
|
+
with tqdm(total=2, desc="Generating explanation", ncols=88) as pbar:
|
|
81
|
+
pbar.set_description("Sending request")
|
|
82
|
+
pbar.update(1)
|
|
83
|
+
|
|
84
|
+
response = client.responses.create(
|
|
85
|
+
model=model,
|
|
86
|
+
instructions=EXPLAIN_PROMPT,
|
|
87
|
+
input=command,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
pbar.set_description("Receiving explanation")
|
|
91
|
+
pbar.update(1)
|
|
92
|
+
|
|
93
|
+
text = getattr(response, "output_text", "") or ""
|
|
94
|
+
return text.strip()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
98
|
+
parser = argparse.ArgumentParser(
|
|
99
|
+
prog="gptc",
|
|
100
|
+
description="Convert natural language into terminal commands for macOS and Linux.",
|
|
101
|
+
)
|
|
102
|
+
parser.add_argument("question", nargs="*", help="Your natural language request.")
|
|
103
|
+
parser.add_argument("--copy", action="store_true", help="Copy the result to the clipboard.")
|
|
104
|
+
parser.add_argument("--explain", action="store_true", help="Show a short explanation of the command.")
|
|
105
|
+
parser.add_argument("--run", action="store_true", help="Run the generated command after confirmation.")
|
|
106
|
+
parser.add_argument("--model", type=str, default=None, help="Specify the model to use.")
|
|
107
|
+
parser.add_argument("--history", action="store_true", help="Show recent command history.")
|
|
108
|
+
return parser
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def main() -> None:
|
|
112
|
+
parser = build_parser()
|
|
113
|
+
args = parser.parse_args()
|
|
114
|
+
|
|
115
|
+
if args.history:
|
|
116
|
+
print_history(limit=10)
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
if not args.question:
|
|
120
|
+
parser.print_help()
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
|
|
123
|
+
question = " ".join(args.question).strip()
|
|
124
|
+
model = args.model or get_default_model()
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
command = ask_model_for_command(question, model=model)
|
|
128
|
+
except KeyboardInterrupt:
|
|
129
|
+
print("\nInterrupted.")
|
|
130
|
+
sys.exit(130)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
133
|
+
sys.exit(1)
|
|
134
|
+
|
|
135
|
+
print(command)
|
|
136
|
+
|
|
137
|
+
explained = False
|
|
138
|
+
if args.explain:
|
|
139
|
+
try:
|
|
140
|
+
explanation = ask_model_for_explanation(command, model=model)
|
|
141
|
+
if explanation:
|
|
142
|
+
print("\n[Explanation]")
|
|
143
|
+
print(explanation)
|
|
144
|
+
explained = True
|
|
145
|
+
except Exception as e:
|
|
146
|
+
print(f"\nExplanation failed: {e}")
|
|
147
|
+
|
|
148
|
+
copied = False
|
|
149
|
+
if args.copy:
|
|
150
|
+
copied = copy_to_clipboard(command)
|
|
151
|
+
if copied:
|
|
152
|
+
print("\nCopied to clipboard.")
|
|
153
|
+
else:
|
|
154
|
+
print("\nFailed to copy to clipboard. Check pbcopy, xclip, or wl-copy.")
|
|
155
|
+
|
|
156
|
+
if is_dangerous_command(command):
|
|
157
|
+
print(
|
|
158
|
+
"\n[Warning] A potentially dangerous command pattern was detected. "
|
|
159
|
+
"Auto-run and auto-prefill have been blocked."
|
|
160
|
+
)
|
|
161
|
+
save_history_item(
|
|
162
|
+
question=question,
|
|
163
|
+
command=command,
|
|
164
|
+
executed=False,
|
|
165
|
+
copied=copied,
|
|
166
|
+
explained=explained,
|
|
167
|
+
)
|
|
168
|
+
sys.exit(2)
|
|
169
|
+
|
|
170
|
+
if args.run:
|
|
171
|
+
print()
|
|
172
|
+
if yes_no("Do you want to run this command?", default="n"):
|
|
173
|
+
code = run_shell_command(command)
|
|
174
|
+
print(f"\nExit code: {code}")
|
|
175
|
+
save_history_item(
|
|
176
|
+
question=question,
|
|
177
|
+
command=command,
|
|
178
|
+
executed=True,
|
|
179
|
+
copied=copied,
|
|
180
|
+
explained=explained,
|
|
181
|
+
)
|
|
182
|
+
sys.exit(code)
|
|
183
|
+
else:
|
|
184
|
+
print("Execution cancelled.")
|
|
185
|
+
save_history_item(
|
|
186
|
+
question=question,
|
|
187
|
+
command=command,
|
|
188
|
+
executed=False,
|
|
189
|
+
copied=copied,
|
|
190
|
+
explained=explained,
|
|
191
|
+
)
|
|
192
|
+
sys.exit(0)
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
print()
|
|
196
|
+
entered = prefilled_input(command)
|
|
197
|
+
if entered.strip():
|
|
198
|
+
print("Input prefilled successfully.")
|
|
199
|
+
except KeyboardInterrupt:
|
|
200
|
+
print("\nCancelled.")
|
|
201
|
+
save_history_item(
|
|
202
|
+
question=question,
|
|
203
|
+
command=command,
|
|
204
|
+
executed=False,
|
|
205
|
+
copied=copied,
|
|
206
|
+
explained=explained,
|
|
207
|
+
)
|
|
208
|
+
sys.exit(130)
|
|
209
|
+
except EOFError:
|
|
210
|
+
print()
|
|
211
|
+
save_history_item(
|
|
212
|
+
question=question,
|
|
213
|
+
command=command,
|
|
214
|
+
executed=False,
|
|
215
|
+
copied=copied,
|
|
216
|
+
explained=explained,
|
|
217
|
+
)
|
|
218
|
+
sys.exit(0)
|
|
219
|
+
|
|
220
|
+
save_history_item(
|
|
221
|
+
question=question,
|
|
222
|
+
command=command,
|
|
223
|
+
executed=False,
|
|
224
|
+
copied=copied,
|
|
225
|
+
explained=explained,
|
|
226
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
APP_NAME = "gptc"
|
|
7
|
+
CONFIG_DIR = Path.home() / ".config" / APP_NAME
|
|
8
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
9
|
+
HISTORY_FILE = CONFIG_DIR / "history.json"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def ensure_config_dir() -> None:
|
|
13
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_json_file(path: Path, default: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
17
|
+
if default is None:
|
|
18
|
+
default = {}
|
|
19
|
+
|
|
20
|
+
if not path.exists():
|
|
21
|
+
return default
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
25
|
+
return json.load(f)
|
|
26
|
+
except Exception:
|
|
27
|
+
return default
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def save_json_file(path: Path, data: Dict[str, Any]) -> None:
|
|
31
|
+
ensure_config_dir()
|
|
32
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
33
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
34
|
+
os.chmod(path, 0o600)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_config() -> Dict[str, Any]:
|
|
38
|
+
return load_json_file(CONFIG_FILE, default={})
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def save_config(data: Dict[str, Any]) -> None:
|
|
42
|
+
save_json_file(CONFIG_FILE, data)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_api_key() -> Optional[str]:
|
|
46
|
+
# 1순위: 환경변수
|
|
47
|
+
env_key = os.environ.get("OPENAI_API_KEY")
|
|
48
|
+
if env_key:
|
|
49
|
+
return env_key.strip()
|
|
50
|
+
|
|
51
|
+
# 2순위: 로컬 설정 파일
|
|
52
|
+
config = load_config()
|
|
53
|
+
key = config.get("OPENAI_API_KEY")
|
|
54
|
+
if isinstance(key, str) and key.strip():
|
|
55
|
+
return key.strip()
|
|
56
|
+
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_default_model() -> str:
|
|
61
|
+
env_model = os.environ.get("OPENAI_MODEL")
|
|
62
|
+
if env_model and env_model.strip():
|
|
63
|
+
return env_model.strip()
|
|
64
|
+
|
|
65
|
+
config = load_config()
|
|
66
|
+
model = config.get("DEFAULT_MODEL")
|
|
67
|
+
if isinstance(model, str) and model.strip():
|
|
68
|
+
return model.strip()
|
|
69
|
+
|
|
70
|
+
return "gpt-4.1"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def set_default_model(model: str) -> None:
|
|
74
|
+
config = load_config()
|
|
75
|
+
config["DEFAULT_MODEL"] = model
|
|
76
|
+
save_config(config)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
|
|
4
|
+
from .config import HISTORY_FILE, load_json_file, save_json_file
|
|
5
|
+
|
|
6
|
+
MAX_HISTORY = 50
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_history() -> List[Dict[str, Any]]:
|
|
10
|
+
data = load_json_file(HISTORY_FILE, default={"items": []})
|
|
11
|
+
items = data.get("items", [])
|
|
12
|
+
if isinstance(items, list):
|
|
13
|
+
return items
|
|
14
|
+
return []
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def save_history_item(
|
|
18
|
+
question: str,
|
|
19
|
+
command: str,
|
|
20
|
+
executed: bool = False,
|
|
21
|
+
copied: bool = False,
|
|
22
|
+
explained: bool = False,
|
|
23
|
+
) -> None:
|
|
24
|
+
items = load_history()
|
|
25
|
+
items.insert(
|
|
26
|
+
0,
|
|
27
|
+
{
|
|
28
|
+
"timestamp": datetime.now().isoformat(timespec="seconds"),
|
|
29
|
+
"question": question,
|
|
30
|
+
"command": command,
|
|
31
|
+
"executed": executed,
|
|
32
|
+
"copied": copied,
|
|
33
|
+
"explained": explained,
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
items = items[:MAX_HISTORY]
|
|
37
|
+
save_json_file(HISTORY_FILE, {"items": items})
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def print_history(limit: int = 10) -> None:
|
|
41
|
+
items = load_history()[:limit]
|
|
42
|
+
if not items:
|
|
43
|
+
print("히스토리가 없습니다.")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
for i, item in enumerate(items, start=1):
|
|
47
|
+
ts = item.get("timestamp", "-")
|
|
48
|
+
q = item.get("question", "")
|
|
49
|
+
cmd = item.get("command", "")
|
|
50
|
+
print(f"[{i}] {ts}")
|
|
51
|
+
print(f" 질문: {q}")
|
|
52
|
+
print(f" 명령: {cmd}")
|
|
53
|
+
print()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import getpass
|
|
3
|
+
|
|
4
|
+
from .config import CONFIG_FILE, get_api_key, load_config, save_config, set_default_model
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def set_api_key() -> None:
|
|
8
|
+
print("Enter your OpenAI API key. Input will be hidden.")
|
|
9
|
+
key = getpass.getpass("API Key: ").strip()
|
|
10
|
+
|
|
11
|
+
if not key:
|
|
12
|
+
print("No key was entered.")
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
config = load_config()
|
|
16
|
+
config["OPENAI_API_KEY"] = key
|
|
17
|
+
save_config(config)
|
|
18
|
+
print(f"API key saved: {CONFIG_FILE}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def delete_api_key() -> None:
|
|
22
|
+
config = load_config()
|
|
23
|
+
if "OPENAI_API_KEY" in config:
|
|
24
|
+
del config["OPENAI_API_KEY"]
|
|
25
|
+
save_config(config)
|
|
26
|
+
print("Stored API key deleted.")
|
|
27
|
+
else:
|
|
28
|
+
print("No stored API key found.")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def show_status() -> None:
|
|
32
|
+
key = get_api_key()
|
|
33
|
+
if key:
|
|
34
|
+
masked = key[:7] + "*" * max(0, len(key) - 11) + key[-4:]
|
|
35
|
+
print("API key is configured.")
|
|
36
|
+
print(f"Key preview: {masked}")
|
|
37
|
+
else:
|
|
38
|
+
print("API key is not configured.")
|
|
39
|
+
|
|
40
|
+
config = load_config()
|
|
41
|
+
model = config.get("DEFAULT_MODEL", "gpt-4.1")
|
|
42
|
+
print(f"Default model: {model}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def set_model(model: str) -> None:
|
|
46
|
+
set_default_model(model)
|
|
47
|
+
print(f"Default model saved: {model}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main() -> None:
|
|
51
|
+
parser = argparse.ArgumentParser(
|
|
52
|
+
prog="gptc-key",
|
|
53
|
+
description="Manage gptc API key and default model settings.",
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument("--set", action="store_true", help="Set the API key.")
|
|
56
|
+
parser.add_argument("--delete", action="store_true", help="Delete the stored API key.")
|
|
57
|
+
parser.add_argument("--status", action="store_true", help="Show current configuration status.")
|
|
58
|
+
parser.add_argument("--model", type=str, help="Set the default model.")
|
|
59
|
+
|
|
60
|
+
args = parser.parse_args()
|
|
61
|
+
acted = False
|
|
62
|
+
|
|
63
|
+
if args.set:
|
|
64
|
+
set_api_key()
|
|
65
|
+
acted = True
|
|
66
|
+
|
|
67
|
+
if args.delete:
|
|
68
|
+
delete_api_key()
|
|
69
|
+
acted = True
|
|
70
|
+
|
|
71
|
+
if args.status:
|
|
72
|
+
show_status()
|
|
73
|
+
acted = True
|
|
74
|
+
|
|
75
|
+
if args.model:
|
|
76
|
+
set_model(args.model)
|
|
77
|
+
acted = True
|
|
78
|
+
|
|
79
|
+
if not acted:
|
|
80
|
+
set_api_key()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import readline
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DANGEROUS_PATTERNS = [
|
|
11
|
+
"rm -rf /",
|
|
12
|
+
"rm -rf ~",
|
|
13
|
+
"mkfs",
|
|
14
|
+
"dd if=",
|
|
15
|
+
":(){ :|:& };:",
|
|
16
|
+
"shutdown -h now",
|
|
17
|
+
"reboot",
|
|
18
|
+
"poweroff",
|
|
19
|
+
"halt",
|
|
20
|
+
"chmod -R 777 /",
|
|
21
|
+
"chown -R",
|
|
22
|
+
"> /dev/sda",
|
|
23
|
+
"> /dev/nvme",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_dangerous_command(cmd: str) -> bool:
|
|
28
|
+
lowered = cmd.lower()
|
|
29
|
+
return any(p.lower() in lowered for p in DANGEROUS_PATTERNS)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_prompt() -> str:
|
|
33
|
+
cwd = Path.cwd()
|
|
34
|
+
home = Path.home()
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
cwd_str = str(cwd)
|
|
38
|
+
home_str = str(home)
|
|
39
|
+
if cwd_str.startswith(home_str):
|
|
40
|
+
cwd_display = cwd_str.replace(home_str, "~", 1)
|
|
41
|
+
else:
|
|
42
|
+
cwd_display = cwd_str
|
|
43
|
+
except Exception:
|
|
44
|
+
cwd_display = str(cwd)
|
|
45
|
+
|
|
46
|
+
return f"{cwd_display}$ "
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def prefilled_input(prefill: str, prompt: Optional[str] = None) -> str:
|
|
50
|
+
if prompt is None:
|
|
51
|
+
prompt = build_prompt()
|
|
52
|
+
|
|
53
|
+
def hook() -> None:
|
|
54
|
+
readline.insert_text(prefill)
|
|
55
|
+
readline.redisplay()
|
|
56
|
+
|
|
57
|
+
old_hook = readline.get_startup_hook()
|
|
58
|
+
try:
|
|
59
|
+
readline.set_startup_hook(hook)
|
|
60
|
+
return input(prompt)
|
|
61
|
+
finally:
|
|
62
|
+
readline.set_startup_hook(old_hook)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def extract_command(text: str) -> str:
|
|
66
|
+
cmd = text.strip()
|
|
67
|
+
|
|
68
|
+
if cmd.startswith("```"):
|
|
69
|
+
lines = [line for line in cmd.splitlines() if not line.strip().startswith("```")]
|
|
70
|
+
cmd = "\n".join(lines).strip()
|
|
71
|
+
|
|
72
|
+
if "\n" in cmd:
|
|
73
|
+
cmd = cmd.splitlines()[0].strip()
|
|
74
|
+
|
|
75
|
+
cmd = cmd.strip().strip("`").strip()
|
|
76
|
+
cmd = cmd.strip('"').strip("'").strip()
|
|
77
|
+
|
|
78
|
+
return cmd
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def copy_to_clipboard(text: str) -> bool:
|
|
82
|
+
system = platform.system()
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
if system == "Darwin" and shutil.which("pbcopy"):
|
|
86
|
+
subprocess.run(["pbcopy"], input=text, text=True, check=True)
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
if shutil.which("wl-copy"):
|
|
90
|
+
subprocess.run(["wl-copy"], input=text, text=True, check=True)
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
if shutil.which("xclip"):
|
|
94
|
+
subprocess.run(["xclip", "-selection", "clipboard"], input=text, text=True, check=True)
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
return False
|
|
98
|
+
except Exception:
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def run_shell_command(cmd: str) -> int:
|
|
103
|
+
try:
|
|
104
|
+
completed = subprocess.run(cmd, shell=True)
|
|
105
|
+
return completed.returncode
|
|
106
|
+
except KeyboardInterrupt:
|
|
107
|
+
return 130
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def yes_no(question: str, default: str = "n") -> bool:
|
|
111
|
+
default = default.lower().strip()
|
|
112
|
+
prompt = " [Y/n] " if default == "y" else " [y/N] "
|
|
113
|
+
|
|
114
|
+
answer = input(question + prompt).strip().lower()
|
|
115
|
+
if not answer:
|
|
116
|
+
return default == "y"
|
|
117
|
+
return answer in {"y", "yes"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gpt-command
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Ask GPT for Ubuntu/macOS/Linux shell commands from natural language
|
|
5
|
+
Author: Your Name
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: openai>=2.0.0
|
|
9
|
+
Requires-Dist: tqdm>=4.66.0
|
|
10
|
+
|
|
11
|
+
# gpt-command (gptc)
|
|
12
|
+
|
|
13
|
+
> Turn natural language into ready-to-use terminal commands.
|
|
14
|
+
|
|
15
|
+
`gptc` is a lightweight CLI tool that converts plain English (or any language) into executable shell commands for macOS and Linux.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🚀 Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install gpt-command
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
🔑 Setup API Key
|
|
28
|
+
|
|
29
|
+
Run once to store your OpenAI API key securely:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
gptc-key
|
|
33
|
+
```
|
|
34
|
+
Check status:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
gptc-key --status
|
|
38
|
+
```
|
|
39
|
+
Set default model (optional):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
gptc-key --model gpt-4.1
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
💡 Usage
|
|
48
|
+
```bash
|
|
49
|
+
gptc <your question>
|
|
50
|
+
```
|
|
51
|
+
Example
|
|
52
|
+
```bash
|
|
53
|
+
gptc find and delete all txt files in current directory recursively
|
|
54
|
+
```
|
|
55
|
+
Output:
|
|
56
|
+
```bash
|
|
57
|
+
find . -type f -name "*.txt" -delete
|
|
58
|
+
```
|
|
59
|
+
Then it will automatically prefill your terminal input:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
~/current/path$ find . -type f -name "*.txt" -delete
|
|
63
|
+
```
|
|
64
|
+
⚠️ The command is NOT executed automatically.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
⚙️ Options
|
|
69
|
+
|
|
70
|
+
📋 Copy to clipboard
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
gptc --copy compress current folder into tar.gz
|
|
74
|
+
```
|
|
75
|
+
📖 Show explanation
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
gptc --explain find process using port 8000
|
|
79
|
+
```
|
|
80
|
+
▶️ Execute command (with confirmation)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
gptc --run check disk usage
|
|
84
|
+
```
|
|
85
|
+
🧠 Show history
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
gptc --history
|
|
89
|
+
```
|
|
90
|
+
🤖 Specify model
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
gptc --model gpt-4.1 list all running processes
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
🔐 Security
|
|
99
|
+
|
|
100
|
+
- API key is stored locally at:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
~/.config/gptc/config.json
|
|
104
|
+
```
|
|
105
|
+
File permissions are restricted to the user (600)
|
|
106
|
+
Dangerous commands are automatically detected and blocked from execution
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
⚠️ Disclaimer
|
|
111
|
+
- Always review generated commands before running them
|
|
112
|
+
- Some commands may modify or delete system data
|
|
113
|
+
- Use with caution, especially with elevated privileges (sudo)
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
🖥️ Supported Platforms
|
|
118
|
+
|
|
119
|
+
- macOS
|
|
120
|
+
- Linux (Ubuntu, etc.)
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
✨ Features
|
|
125
|
+
- Natural language → shell command
|
|
126
|
+
- Auto-prefilled terminal input (no copy-paste needed)
|
|
127
|
+
- Optional execution with confirmation
|
|
128
|
+
- Clipboard copy support
|
|
129
|
+
- Command explanation
|
|
130
|
+
- History tracking
|
|
131
|
+
- Local API key management (no environment variable required)
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
📌 Summary
|
|
136
|
+
|
|
137
|
+
> Stop Googling terminal commands. Just ask.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
📄 License
|
|
142
|
+
|
|
143
|
+
MIT License
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/gpt_command/__init__.py
|
|
4
|
+
src/gpt_command/cli.py
|
|
5
|
+
src/gpt_command/config.py
|
|
6
|
+
src/gpt_command/history.py
|
|
7
|
+
src/gpt_command/key_manager.py
|
|
8
|
+
src/gpt_command/utils.py
|
|
9
|
+
src/gpt_command.egg-info/PKG-INFO
|
|
10
|
+
src/gpt_command.egg-info/SOURCES.txt
|
|
11
|
+
src/gpt_command.egg-info/dependency_links.txt
|
|
12
|
+
src/gpt_command.egg-info/entry_points.txt
|
|
13
|
+
src/gpt_command.egg-info/requires.txt
|
|
14
|
+
src/gpt_command.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gpt_command
|