bro-cli 0.1.1.4__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.
- bro_cli-0.1.1.4/PKG-INFO +92 -0
- bro_cli-0.1.1.4/README.md +80 -0
- bro_cli-0.1.1.4/bro_cli/__init__.py +4 -0
- bro_cli-0.1.1.4/bro_cli/config.py +67 -0
- bro_cli-0.1.1.4/bro_cli/gemini_client.py +112 -0
- bro_cli-0.1.1.4/bro_cli/main.py +156 -0
- bro_cli-0.1.1.4/bro_cli.egg-info/PKG-INFO +92 -0
- bro_cli-0.1.1.4/bro_cli.egg-info/SOURCES.txt +16 -0
- bro_cli-0.1.1.4/bro_cli.egg-info/dependency_links.txt +1 -0
- bro_cli-0.1.1.4/bro_cli.egg-info/entry_points.txt +2 -0
- bro_cli-0.1.1.4/bro_cli.egg-info/requires.txt +5 -0
- bro_cli-0.1.1.4/bro_cli.egg-info/top_level.txt +1 -0
- bro_cli-0.1.1.4/pyproject.toml +34 -0
- bro_cli-0.1.1.4/setup.cfg +4 -0
bro_cli-0.1.1.4/PKG-INFO
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bro-cli
|
|
3
|
+
Version: 0.1.1.4
|
|
4
|
+
Summary: Linux terminal Gemini client with secure local API key config
|
|
5
|
+
Author: Aether
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: google-genai>=1.20.0
|
|
9
|
+
Requires-Dist: rich
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
12
|
+
|
|
13
|
+
# Bro CLI
|
|
14
|
+
|
|
15
|
+
A lightweight, fast, and secure Linux terminal client for Google's Gemini AI.
|
|
16
|
+
|
|
17
|
+
`bro` allows you to interact with Gemini directly from your command line, offering both single-prompt execution and a persistent interactive chat mode. It securely manages your API credentials using standard Linux XDG directories.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Interactive & Single-Prompt Modes:** Seamlessly switch between quick one-off questions and interactive conversational sessions.
|
|
22
|
+
- **Secure Credential Management:** API keys are stored securely in `~/.config/bro/config.json` with strict user-only (`0600`) file permissions.
|
|
23
|
+
- **Environment Variable Support:** Override stored configurations on the fly using the `BRO_GEMINI_KEY` environment variable.
|
|
24
|
+
- **Terminal-Optimized Output:** Instructs Gemini to return direct, concise answers without unnecessary markdown formatting or conversational filler.
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
- Linux
|
|
29
|
+
- Python 3.10 or higher
|
|
30
|
+
- [pipx](https://pipx.pypa.io/) (Required for isolated global CLI installation)
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Install `bro-cli` globally as an isolated application using `pipx`.
|
|
35
|
+
|
|
36
|
+
To install directly from your Git repository:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pipx install git+https://github.com/PromitSarker/Bro-CLI.git
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Local Installation:**
|
|
43
|
+
If you have cloned the repository to your local machine, you can navigate to the project folder and install it by running:
|
|
44
|
+
```bash
|
|
45
|
+
pipx install .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Updating:**
|
|
49
|
+
To upgrade to the latest version in the future:
|
|
50
|
+
```bash
|
|
51
|
+
pipx upgrade bro-cli
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
Before using the client, you *must* configure your Google Gemini API key. Run the following command:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
bro config
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
You will be prompted to paste your API key. It will be saved securely to your system.
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
### Single Prompt
|
|
67
|
+
|
|
68
|
+
Ask a direct question. The client will return the answer to your standard output and exit:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bro "Why life so hard ?"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
*Note: You can also skip the quotes for simple queries:*
|
|
75
|
+
```bash
|
|
76
|
+
bro how to go and breathe ?
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Interactive Mode
|
|
80
|
+
|
|
81
|
+
Start a multi-turn chat session by running the command without any arguments:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
bro
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
*Inside the interactive prompt, type `exit`, `quit`, or press `Ctrl+C`/`Ctrl+D` to end the session.*
|
|
88
|
+
|
|
89
|
+
## Technical Details
|
|
90
|
+
|
|
91
|
+
- **Default Model:** `gemini-2.5-flash-lite`
|
|
92
|
+
- **SDK:** Powered by the official `google-genai` Python SDK.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Bro CLI
|
|
2
|
+
|
|
3
|
+
A lightweight, fast, and secure Linux terminal client for Google's Gemini AI.
|
|
4
|
+
|
|
5
|
+
`bro` allows you to interact with Gemini directly from your command line, offering both single-prompt execution and a persistent interactive chat mode. It securely manages your API credentials using standard Linux XDG directories.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Interactive & Single-Prompt Modes:** Seamlessly switch between quick one-off questions and interactive conversational sessions.
|
|
10
|
+
- **Secure Credential Management:** API keys are stored securely in `~/.config/bro/config.json` with strict user-only (`0600`) file permissions.
|
|
11
|
+
- **Environment Variable Support:** Override stored configurations on the fly using the `BRO_GEMINI_KEY` environment variable.
|
|
12
|
+
- **Terminal-Optimized Output:** Instructs Gemini to return direct, concise answers without unnecessary markdown formatting or conversational filler.
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- Linux
|
|
17
|
+
- Python 3.10 or higher
|
|
18
|
+
- [pipx](https://pipx.pypa.io/) (Required for isolated global CLI installation)
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Install `bro-cli` globally as an isolated application using `pipx`.
|
|
23
|
+
|
|
24
|
+
To install directly from your Git repository:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pipx install git+https://github.com/PromitSarker/Bro-CLI.git
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Local Installation:**
|
|
31
|
+
If you have cloned the repository to your local machine, you can navigate to the project folder and install it by running:
|
|
32
|
+
```bash
|
|
33
|
+
pipx install .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Updating:**
|
|
37
|
+
To upgrade to the latest version in the future:
|
|
38
|
+
```bash
|
|
39
|
+
pipx upgrade bro-cli
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
44
|
+
Before using the client, you *must* configure your Google Gemini API key. Run the following command:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bro config
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
You will be prompted to paste your API key. It will be saved securely to your system.
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### Single Prompt
|
|
55
|
+
|
|
56
|
+
Ask a direct question. The client will return the answer to your standard output and exit:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
bro "Why life so hard ?"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
*Note: You can also skip the quotes for simple queries:*
|
|
63
|
+
```bash
|
|
64
|
+
bro how to go and breathe ?
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Interactive Mode
|
|
68
|
+
|
|
69
|
+
Start a multi-turn chat session by running the command without any arguments:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
bro
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
*Inside the interactive prompt, type `exit`, `quit`, or press `Ctrl+C`/`Ctrl+D` to end the session.*
|
|
76
|
+
|
|
77
|
+
## Technical Details
|
|
78
|
+
|
|
79
|
+
- **Default Model:** `gemini-2.5-flash-lite`
|
|
80
|
+
- **SDK:** Powered by the official `google-genai` Python SDK.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
ENV_API_KEY = "BRO_GEMINI_KEY"
|
|
9
|
+
_CONFIG_DIR_NAME = "bro"
|
|
10
|
+
_CONFIG_FILE_NAME = "config.json"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_config_path() -> Path:
|
|
14
|
+
"""Return the Linux-friendly config path for bro."""
|
|
15
|
+
xdg_config_home = os.environ.get("XDG_CONFIG_HOME")
|
|
16
|
+
if xdg_config_home:
|
|
17
|
+
base_dir = Path(xdg_config_home)
|
|
18
|
+
else:
|
|
19
|
+
base_dir = Path.home() / ".config"
|
|
20
|
+
return base_dir / _CONFIG_DIR_NAME / _CONFIG_FILE_NAME
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def prompt_for_api_key() -> str:
|
|
24
|
+
"""Prompt user for API key until a non-empty value is provided."""
|
|
25
|
+
while True:
|
|
26
|
+
value = input("Enter your Gemini API key: ").strip()
|
|
27
|
+
if value:
|
|
28
|
+
return value
|
|
29
|
+
print("API key cannot be empty.")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def save_api_key(api_key: str) -> Path:
|
|
33
|
+
"""Persist API key to config file with mode 0600."""
|
|
34
|
+
config_path = get_config_path()
|
|
35
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
|
|
37
|
+
with config_path.open("w", encoding="utf-8") as f:
|
|
38
|
+
json.dump({"api_key": api_key}, f)
|
|
39
|
+
|
|
40
|
+
os.chmod(config_path, 0o600)
|
|
41
|
+
return config_path
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_api_key_from_file() -> Optional[str]:
|
|
45
|
+
"""Read API key from config file when available and valid."""
|
|
46
|
+
config_path = get_config_path()
|
|
47
|
+
if not config_path.exists():
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
with config_path.open("r", encoding="utf-8") as f:
|
|
52
|
+
data = json.load(f)
|
|
53
|
+
except (OSError, json.JSONDecodeError):
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
value = data.get("api_key") if isinstance(data, dict) else None
|
|
57
|
+
if not value or not isinstance(value, str):
|
|
58
|
+
return None
|
|
59
|
+
return value.strip() or None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def resolve_api_key() -> Optional[str]:
|
|
63
|
+
"""Resolve key from env first, then config file."""
|
|
64
|
+
env_value = os.environ.get(ENV_API_KEY, "").strip()
|
|
65
|
+
if env_value:
|
|
66
|
+
return env_value
|
|
67
|
+
return load_api_key_from_file()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from google import genai
|
|
7
|
+
from google.genai import types
|
|
8
|
+
except ImportError: # pragma: no cover - depends on local install state
|
|
9
|
+
genai = None
|
|
10
|
+
types = None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
SYSTEM_INSTRUCTION = (
|
|
14
|
+
"You are a Linux terminal assistant with a friendly, human-like personality. "
|
|
15
|
+
"Act like a smart, slightly witty buddy who helps efficiently. "
|
|
16
|
+
"Keep responses concise but not robotic. "
|
|
17
|
+
"You may use light humor, casual phrasing, and opinions when appropriate. "
|
|
18
|
+
"Avoid long explanations unless asked. "
|
|
19
|
+
"No markdown formatting. Plain text only. "
|
|
20
|
+
"Prioritize clarity and usefulness over strict brevity."
|
|
21
|
+
"Sound like a developer friend, not a documentation page."
|
|
22
|
+
"STRICT RULE: NEVER use Markdown symbols. NO asterisks (*), NO hashtags (#), "
|
|
23
|
+
"NO bold text, NO bullet points using *. "
|
|
24
|
+
"For lists, use plain dashes (-) or simple indentation. "
|
|
25
|
+
"Your output must be pure plain text that looks good in a raw terminal. "
|
|
26
|
+
"Sound like a developer friend chatting, not a documentation page."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ClientError(Exception):
|
|
33
|
+
message: str
|
|
34
|
+
exit_code: int = 1
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class GeminiClient:
|
|
38
|
+
def __init__(self, api_key: str, use_search: bool = False) -> None:
|
|
39
|
+
if genai is None or types is None:
|
|
40
|
+
raise ClientError(
|
|
41
|
+
"Missing dependency: google-genai. Install bro again with its Python dependencies.",
|
|
42
|
+
exit_code=1,
|
|
43
|
+
)
|
|
44
|
+
self._client = genai.Client(api_key=api_key)
|
|
45
|
+
self._use_search = use_search
|
|
46
|
+
# Use a model that supports search for search tasks, and lite for normal tasks.
|
|
47
|
+
self._model = "gemini-3-flash-preview" if use_search else "gemini-2.5-flash-lite"
|
|
48
|
+
|
|
49
|
+
def _get_config(self) -> types.GenerateContentConfig:
|
|
50
|
+
config = types.GenerateContentConfig(
|
|
51
|
+
system_instruction=SYSTEM_INSTRUCTION,
|
|
52
|
+
max_output_tokens=500, # Enforce brevity at the API level
|
|
53
|
+
)
|
|
54
|
+
if self._use_search:
|
|
55
|
+
config.tools = [
|
|
56
|
+
types.Tool(
|
|
57
|
+
google_search=types.GoogleSearch()
|
|
58
|
+
)
|
|
59
|
+
]
|
|
60
|
+
return config
|
|
61
|
+
|
|
62
|
+
def ask(self, prompt: str) -> str:
|
|
63
|
+
response = self._client.models.generate_content(
|
|
64
|
+
model=self._model,
|
|
65
|
+
contents=prompt,
|
|
66
|
+
config=self._get_config(),
|
|
67
|
+
)
|
|
68
|
+
text = getattr(response, "text", None)
|
|
69
|
+
if text and text.strip():
|
|
70
|
+
return text.strip()
|
|
71
|
+
return "No response text returned by Gemini."
|
|
72
|
+
|
|
73
|
+
def start_chat(self) -> "GeminiChatSession":
|
|
74
|
+
return GeminiChatSession(
|
|
75
|
+
self._client.chats.create(
|
|
76
|
+
model=self._model,
|
|
77
|
+
history=[],
|
|
78
|
+
config=self._get_config(),
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class GeminiChatSession:
|
|
84
|
+
def __init__(self, chat_session) -> None:
|
|
85
|
+
self._chat_session = chat_session
|
|
86
|
+
|
|
87
|
+
def ask(self, prompt: str) -> str:
|
|
88
|
+
response = self._chat_session.send_message(prompt)
|
|
89
|
+
text = getattr(response, "text", None)
|
|
90
|
+
if text and text.strip():
|
|
91
|
+
return text.strip()
|
|
92
|
+
return "No response text returned by Gemini."
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def map_exception(exc: Exception) -> ClientError:
|
|
96
|
+
"""Map Gemini SDK and transport errors to stable CLI messages."""
|
|
97
|
+
exc_name = exc.__class__.__name__.lower()
|
|
98
|
+
raw_message = str(exc).lower()
|
|
99
|
+
|
|
100
|
+
if "unauth" in exc_name or "permission" in exc_name or "api key" in raw_message:
|
|
101
|
+
return ClientError("API key rejected. Run 'bro config' to update it.", exit_code=1)
|
|
102
|
+
|
|
103
|
+
if "resourceexhausted" in exc_name or "ratelimit" in raw_message or "429" in raw_message:
|
|
104
|
+
return ClientError("Rate limited by Gemini. Please retry shortly.", exit_code=2)
|
|
105
|
+
|
|
106
|
+
if "serviceunavailable" in exc_name or "503" in raw_message:
|
|
107
|
+
return ClientError("Gemini service unavailable. Retry in a moment.", exit_code=2)
|
|
108
|
+
|
|
109
|
+
if "connection" in raw_message or "network" in raw_message or "timeout" in raw_message:
|
|
110
|
+
return ClientError("Network error while contacting Gemini.", exit_code=2)
|
|
111
|
+
|
|
112
|
+
return ClientError(f"Unexpected Gemini error: {exc}", exit_code=2)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Sequence
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.prompt import Prompt
|
|
10
|
+
from rich.theme import Theme
|
|
11
|
+
|
|
12
|
+
from .config import get_config_path, prompt_for_api_key, resolve_api_key, save_api_key
|
|
13
|
+
from .gemini_client import ClientError, GeminiClient, map_exception
|
|
14
|
+
|
|
15
|
+
# Professional Cyberpunk Theme
|
|
16
|
+
custom_theme = Theme({
|
|
17
|
+
"info": "cyan",
|
|
18
|
+
"warning": "yellow",
|
|
19
|
+
"error": "bold red",
|
|
20
|
+
"prompt": "bold green",
|
|
21
|
+
"ai_name": "bold magenta",
|
|
22
|
+
})
|
|
23
|
+
console = Console(theme=custom_theme)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
27
|
+
parser = argparse.ArgumentParser(
|
|
28
|
+
prog="bro",
|
|
29
|
+
description="Linux terminal client for chatting with Gemini",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"-s",
|
|
33
|
+
"--search",
|
|
34
|
+
action="store_true",
|
|
35
|
+
help="Enable web search capability (grounding)",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"command",
|
|
39
|
+
nargs="?",
|
|
40
|
+
help="Use config to set the API key, or provide a prompt to chat",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"prompt",
|
|
44
|
+
nargs=argparse.REMAINDER,
|
|
45
|
+
help="Question to ask Gemini",
|
|
46
|
+
)
|
|
47
|
+
return parser
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_config() -> int:
|
|
51
|
+
existing = resolve_api_key()
|
|
52
|
+
if existing:
|
|
53
|
+
print("An API key is already configured. Saving will overwrite the stored key.")
|
|
54
|
+
|
|
55
|
+
api_key = prompt_for_api_key()
|
|
56
|
+
path = save_api_key(api_key)
|
|
57
|
+
print("API key saved.")
|
|
58
|
+
print(f"Config file: {path}")
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _load_client(use_search: bool = False) -> GeminiClient:
|
|
63
|
+
api_key = resolve_api_key()
|
|
64
|
+
if not api_key:
|
|
65
|
+
raise ClientError(
|
|
66
|
+
"Gemini API key is missing. Run 'bro config' to set it.",
|
|
67
|
+
exit_code=1,
|
|
68
|
+
)
|
|
69
|
+
return GeminiClient(api_key=api_key, use_search=use_search)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def run_single_prompt(prompt_text: str, use_search: bool = False) -> int:
|
|
73
|
+
try:
|
|
74
|
+
client = _load_client(use_search=use_search)
|
|
75
|
+
with console.status("[bold cyan]Bro is thinking...", spinner="dots"):
|
|
76
|
+
response = client.ask(prompt_text)
|
|
77
|
+
|
|
78
|
+
console.print(Panel(
|
|
79
|
+
response,
|
|
80
|
+
title="[ai_name]Bro",
|
|
81
|
+
title_align="left",
|
|
82
|
+
border_style="magenta",
|
|
83
|
+
padding=(1, 2)
|
|
84
|
+
))
|
|
85
|
+
return 0
|
|
86
|
+
except ClientError as exc:
|
|
87
|
+
console.print(f"[error]{exc.message}[/error]", style="red")
|
|
88
|
+
return exc.exit_code
|
|
89
|
+
except Exception as exc: # pragma: no cover - defensive layer
|
|
90
|
+
err = map_exception(exc)
|
|
91
|
+
console.print(f"[error]{err.message}[/error]", style="red")
|
|
92
|
+
return err.exit_code
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def run_interactive_chat(use_search: bool = False) -> int:
|
|
96
|
+
try:
|
|
97
|
+
client = _load_client(use_search=use_search)
|
|
98
|
+
chat = client.start_chat()
|
|
99
|
+
except ClientError as exc:
|
|
100
|
+
console.print(f"[error]{exc.message}[/error]")
|
|
101
|
+
return exc.exit_code
|
|
102
|
+
|
|
103
|
+
mode_info = " [dim](Search enabled)[/dim]" if use_search else ""
|
|
104
|
+
console.print(f"[info]Interactive mode{mode_info}. Type 'exit' or 'quit' to leave.[/info]")
|
|
105
|
+
|
|
106
|
+
while True:
|
|
107
|
+
try:
|
|
108
|
+
# Styled prompt using rich.prompt.Prompt or just console.print
|
|
109
|
+
user_input = Prompt.ask("[prompt]bro[/prompt]")
|
|
110
|
+
except (KeyboardInterrupt, EOFError):
|
|
111
|
+
console.print("\n[info]Exiting. Catch ya later![/info]")
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
if not user_input:
|
|
115
|
+
continue
|
|
116
|
+
if user_input.lower() in {"exit", "quit"}:
|
|
117
|
+
console.print("[info]Exiting. Catch ya later![/info]")
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
with console.status("[bold cyan]Thinking...", spinner="dots"):
|
|
122
|
+
response = chat.ask(user_input)
|
|
123
|
+
|
|
124
|
+
console.print(Panel(
|
|
125
|
+
response,
|
|
126
|
+
title="[ai_name]Bro",
|
|
127
|
+
title_align="left",
|
|
128
|
+
border_style="magenta",
|
|
129
|
+
padding=(1, 2)
|
|
130
|
+
))
|
|
131
|
+
except Exception as exc:
|
|
132
|
+
err = map_exception(exc)
|
|
133
|
+
console.print(f"[error]{err.message}[/error]")
|
|
134
|
+
return err.exit_code
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
138
|
+
parser = build_parser()
|
|
139
|
+
args = parser.parse_args(argv)
|
|
140
|
+
|
|
141
|
+
if args.command == "config" and not args.prompt:
|
|
142
|
+
return run_config()
|
|
143
|
+
|
|
144
|
+
if args.command == "config" and args.prompt:
|
|
145
|
+
parser.error("'config' does not accept a chat prompt")
|
|
146
|
+
|
|
147
|
+
if args.command is not None:
|
|
148
|
+
prompt_parts = [args.command, *args.prompt]
|
|
149
|
+
return run_single_prompt(" ".join(prompt_parts).strip(), use_search=args.search)
|
|
150
|
+
|
|
151
|
+
# No prompt and no subcommand means REPL mode.
|
|
152
|
+
return run_interactive_chat(use_search=args.search)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__":
|
|
156
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bro-cli
|
|
3
|
+
Version: 0.1.1.4
|
|
4
|
+
Summary: Linux terminal Gemini client with secure local API key config
|
|
5
|
+
Author: Aether
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: google-genai>=1.20.0
|
|
9
|
+
Requires-Dist: rich
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
12
|
+
|
|
13
|
+
# Bro CLI
|
|
14
|
+
|
|
15
|
+
A lightweight, fast, and secure Linux terminal client for Google's Gemini AI.
|
|
16
|
+
|
|
17
|
+
`bro` allows you to interact with Gemini directly from your command line, offering both single-prompt execution and a persistent interactive chat mode. It securely manages your API credentials using standard Linux XDG directories.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Interactive & Single-Prompt Modes:** Seamlessly switch between quick one-off questions and interactive conversational sessions.
|
|
22
|
+
- **Secure Credential Management:** API keys are stored securely in `~/.config/bro/config.json` with strict user-only (`0600`) file permissions.
|
|
23
|
+
- **Environment Variable Support:** Override stored configurations on the fly using the `BRO_GEMINI_KEY` environment variable.
|
|
24
|
+
- **Terminal-Optimized Output:** Instructs Gemini to return direct, concise answers without unnecessary markdown formatting or conversational filler.
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
- Linux
|
|
29
|
+
- Python 3.10 or higher
|
|
30
|
+
- [pipx](https://pipx.pypa.io/) (Required for isolated global CLI installation)
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
Install `bro-cli` globally as an isolated application using `pipx`.
|
|
35
|
+
|
|
36
|
+
To install directly from your Git repository:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pipx install git+https://github.com/PromitSarker/Bro-CLI.git
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Local Installation:**
|
|
43
|
+
If you have cloned the repository to your local machine, you can navigate to the project folder and install it by running:
|
|
44
|
+
```bash
|
|
45
|
+
pipx install .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Updating:**
|
|
49
|
+
To upgrade to the latest version in the future:
|
|
50
|
+
```bash
|
|
51
|
+
pipx upgrade bro-cli
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
Before using the client, you *must* configure your Google Gemini API key. Run the following command:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
bro config
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
You will be prompted to paste your API key. It will be saved securely to your system.
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
### Single Prompt
|
|
67
|
+
|
|
68
|
+
Ask a direct question. The client will return the answer to your standard output and exit:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bro "Why life so hard ?"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
*Note: You can also skip the quotes for simple queries:*
|
|
75
|
+
```bash
|
|
76
|
+
bro how to go and breathe ?
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Interactive Mode
|
|
80
|
+
|
|
81
|
+
Start a multi-turn chat session by running the command without any arguments:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
bro
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
*Inside the interactive prompt, type `exit`, `quit`, or press `Ctrl+C`/`Ctrl+D` to end the session.*
|
|
88
|
+
|
|
89
|
+
## Technical Details
|
|
90
|
+
|
|
91
|
+
- **Default Model:** `gemini-2.5-flash-lite`
|
|
92
|
+
- **SDK:** Powered by the official `google-genai` Python SDK.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
./bro_cli/__init__.py
|
|
4
|
+
./bro_cli/config.py
|
|
5
|
+
./bro_cli/gemini_client.py
|
|
6
|
+
./bro_cli/main.py
|
|
7
|
+
bro_cli/__init__.py
|
|
8
|
+
bro_cli/config.py
|
|
9
|
+
bro_cli/gemini_client.py
|
|
10
|
+
bro_cli/main.py
|
|
11
|
+
bro_cli.egg-info/PKG-INFO
|
|
12
|
+
bro_cli.egg-info/SOURCES.txt
|
|
13
|
+
bro_cli.egg-info/dependency_links.txt
|
|
14
|
+
bro_cli.egg-info/entry_points.txt
|
|
15
|
+
bro_cli.egg-info/requires.txt
|
|
16
|
+
bro_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bro_cli
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bro-cli"
|
|
7
|
+
version = "0.1.1.4"
|
|
8
|
+
description = "Linux terminal Gemini client with secure local API key config"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{ name = "Aether" }]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"google-genai>=1.20.0",
|
|
14
|
+
"rich",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=8.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
bro = "bro_cli.main:main"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools]
|
|
26
|
+
package-dir = {"" = "."}
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["."]
|
|
30
|
+
include = ["bro_cli*"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
testpaths = ["tests"]
|
|
34
|
+
addopts = "-q"
|