cobalt-cli-linux 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.
- cobalt_cli_linux-0.1.0/PKG-INFO +113 -0
- cobalt_cli_linux-0.1.0/README.md +88 -0
- cobalt_cli_linux-0.1.0/pyproject.toml +49 -0
- cobalt_cli_linux-0.1.0/setup.cfg +4 -0
- cobalt_cli_linux-0.1.0/src/cobalt_cli_linux/__init__.py +5 -0
- cobalt_cli_linux-0.1.0/src/cobalt_cli_linux/__main__.py +5 -0
- cobalt_cli_linux-0.1.0/src/cobalt_cli_linux/agent.py +136 -0
- cobalt_cli_linux-0.1.0/src/cobalt_cli_linux/cli.py +57 -0
- cobalt_cli_linux-0.1.0/src/cobalt_cli_linux/config.py +54 -0
- cobalt_cli_linux-0.1.0/src/cobalt_cli_linux/deepseek_client.py +51 -0
- cobalt_cli_linux-0.1.0/src/cobalt_cli_linux/executor.py +55 -0
- cobalt_cli_linux-0.1.0/src/cobalt_cli_linux/groq_client.py +156 -0
- cobalt_cli_linux-0.1.0/src/cobalt_cli_linux/history.py +104 -0
- cobalt_cli_linux-0.1.0/src/cobalt_cli_linux/tui.py +269 -0
- cobalt_cli_linux-0.1.0/src/cobalt_cli_linux.egg-info/PKG-INFO +113 -0
- cobalt_cli_linux-0.1.0/src/cobalt_cli_linux.egg-info/SOURCES.txt +19 -0
- cobalt_cli_linux-0.1.0/src/cobalt_cli_linux.egg-info/dependency_links.txt +1 -0
- cobalt_cli_linux-0.1.0/src/cobalt_cli_linux.egg-info/entry_points.txt +2 -0
- cobalt_cli_linux-0.1.0/src/cobalt_cli_linux.egg-info/requires.txt +7 -0
- cobalt_cli_linux-0.1.0/src/cobalt_cli_linux.egg-info/top_level.txt +1 -0
- cobalt_cli_linux-0.1.0/tests/test_cli.py +108 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cobalt-cli-linux
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agentic Groq-powered CLI assistant for Debian Linux
|
|
5
|
+
Author: Cobalt
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/owall4229/cobalt-cli-linux
|
|
8
|
+
Project-URL: Repository, https://github.com/owall4229/cobalt-cli-linux
|
|
9
|
+
Project-URL: Issues, https://github.com/owall4229/cobalt-cli-linux/issues
|
|
10
|
+
Keywords: cli,groq,agent,linux,assistant
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: httpx>=0.27.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
22
|
+
Requires-Dist: ruff>=0.5.0; extra == "dev"
|
|
23
|
+
Requires-Dist: build>=1.2.0; extra == "dev"
|
|
24
|
+
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
25
|
+
|
|
26
|
+
# cobalt-cli-linux
|
|
27
|
+
|
|
28
|
+
Cobalt is an agentic Groq-powered CLI assistant designed for Debian Linux. It can execute shell commands, read and write local files, maintain a local conversation history, and interact with Groq in a production-friendly Python package layout.
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- Groq chat completions via HTTP client
|
|
33
|
+
- Safe local shell execution with bash
|
|
34
|
+
- Persistent JSON conversation history in the user's home directory
|
|
35
|
+
- File read/write operations for scriptable agent workflows
|
|
36
|
+
- PyPI-ready package metadata and console script entrypoint
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
python -m pip install cobalt-cli-linux
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Built-in key and .env override
|
|
45
|
+
|
|
46
|
+
The CLI ships with a built-in Groq API key and uses it automatically, so end users do not have to add their own key when they install the package.
|
|
47
|
+
|
|
48
|
+
If you want to override it for development or a local custom setup, create a `.env` file in the project root or in your home directory with:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
GROQ_API_KEY="your_groq_key_here"
|
|
52
|
+
GROQ_MODEL="llama-3.3-70b-versatile"
|
|
53
|
+
GROQ_BASE_URL="https://api.groq.com/openai/v1"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The CLI automatically reads `.env` entries when present, but it will keep using the built-in key unless you explicitly set `GROQ_API_KEY`. The default built-in key shipped with the package is:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
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
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Run command
|
|
63
|
+
|
|
64
|
+
### Standard command mode
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
cobalt "List the top 10 files in the current directory"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Full-screen interactive mode
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
cobalt --interactive
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This launches a fullscreen TUI that feels similar to a professional agent console, with the message stream and a compact prompt area instead of a raw shell prompt.
|
|
77
|
+
|
|
78
|
+
You can also pass arguments directly:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
cobalt --api-key "$GROQ_API_KEY" "Show the current user and OS info"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Project layout
|
|
85
|
+
|
|
86
|
+
- src/cobalt_cli_linux/agent.py - orchestration logic for tool use and model responses
|
|
87
|
+
- src/cobalt_cli_linux/cli.py - command-line entrypoint
|
|
88
|
+
- src/cobalt_cli_linux/groq_client.py - Groq HTTP client
|
|
89
|
+
- src/cobalt_cli_linux/executor.py - shell command execution
|
|
90
|
+
- src/cobalt_cli_linux/history.py - local JSON conversation history
|
|
91
|
+
- src/cobalt_cli_linux/config.py - environment-based settings
|
|
92
|
+
|
|
93
|
+
## Publish to PyPI
|
|
94
|
+
|
|
95
|
+
From the project root, you can build and upload the package from the terminal:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
python -m pip install --upgrade build twine
|
|
99
|
+
python -m build
|
|
100
|
+
python -m twine upload dist/*
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Or with the included shortcut:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
make publish
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This creates the source and wheel distributions and uploads them to your configured PyPI account.
|
|
110
|
+
|
|
111
|
+
## Notes
|
|
112
|
+
|
|
113
|
+
This package is intentionally built to work on Debian Linux and uses standard POSIX shell commands. The default history file is stored at ~/.cobalt/history.json.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# cobalt-cli-linux
|
|
2
|
+
|
|
3
|
+
Cobalt is an agentic Groq-powered CLI assistant designed for Debian Linux. It can execute shell commands, read and write local files, maintain a local conversation history, and interact with Groq in a production-friendly Python package layout.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Groq chat completions via HTTP client
|
|
8
|
+
- Safe local shell execution with bash
|
|
9
|
+
- Persistent JSON conversation history in the user's home directory
|
|
10
|
+
- File read/write operations for scriptable agent workflows
|
|
11
|
+
- PyPI-ready package metadata and console script entrypoint
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python -m pip install cobalt-cli-linux
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Built-in key and .env override
|
|
20
|
+
|
|
21
|
+
The CLI ships with a built-in Groq API key and uses it automatically, so end users do not have to add their own key when they install the package.
|
|
22
|
+
|
|
23
|
+
If you want to override it for development or a local custom setup, create a `.env` file in the project root or in your home directory with:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
GROQ_API_KEY="your_groq_key_here"
|
|
27
|
+
GROQ_MODEL="llama-3.3-70b-versatile"
|
|
28
|
+
GROQ_BASE_URL="https://api.groq.com/openai/v1"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The CLI automatically reads `.env` entries when present, but it will keep using the built-in key unless you explicitly set `GROQ_API_KEY`. The default built-in key shipped with the package is:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
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
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Run command
|
|
38
|
+
|
|
39
|
+
### Standard command mode
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cobalt "List the top 10 files in the current directory"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Full-screen interactive mode
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cobalt --interactive
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This launches a fullscreen TUI that feels similar to a professional agent console, with the message stream and a compact prompt area instead of a raw shell prompt.
|
|
52
|
+
|
|
53
|
+
You can also pass arguments directly:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cobalt --api-key "$GROQ_API_KEY" "Show the current user and OS info"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Project layout
|
|
60
|
+
|
|
61
|
+
- src/cobalt_cli_linux/agent.py - orchestration logic for tool use and model responses
|
|
62
|
+
- src/cobalt_cli_linux/cli.py - command-line entrypoint
|
|
63
|
+
- src/cobalt_cli_linux/groq_client.py - Groq HTTP client
|
|
64
|
+
- src/cobalt_cli_linux/executor.py - shell command execution
|
|
65
|
+
- src/cobalt_cli_linux/history.py - local JSON conversation history
|
|
66
|
+
- src/cobalt_cli_linux/config.py - environment-based settings
|
|
67
|
+
|
|
68
|
+
## Publish to PyPI
|
|
69
|
+
|
|
70
|
+
From the project root, you can build and upload the package from the terminal:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python -m pip install --upgrade build twine
|
|
74
|
+
python -m build
|
|
75
|
+
python -m twine upload dist/*
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Or with the included shortcut:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
make publish
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This creates the source and wheel distributions and uploads them to your configured PyPI account.
|
|
85
|
+
|
|
86
|
+
## Notes
|
|
87
|
+
|
|
88
|
+
This package is intentionally built to work on Debian Linux and uses standard POSIX shell commands. The default history file is stored at ~/.cobalt/history.json.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cobalt-cli-linux"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Agentic Groq-powered CLI assistant for Debian Linux"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{ name = "Cobalt" }]
|
|
12
|
+
license = "MIT"
|
|
13
|
+
keywords = ["cli", "groq", "agent", "linux", "assistant"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Operating System :: POSIX :: Linux",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"httpx>=0.27.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/owall4229/cobalt-cli-linux"
|
|
28
|
+
Repository = "https://github.com/owall4229/cobalt-cli-linux"
|
|
29
|
+
Issues = "https://github.com/owall4229/cobalt-cli-linux/issues"
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=8.0.0",
|
|
34
|
+
"ruff>=0.5.0",
|
|
35
|
+
"build>=1.2.0",
|
|
36
|
+
"twine>=5.0.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
cobalt = "cobalt_cli_linux.cli:main"
|
|
41
|
+
|
|
42
|
+
[tool.setuptools]
|
|
43
|
+
package-dir = {"" = "src"}
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.packages.find]
|
|
46
|
+
where = ["src"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .config import Settings
|
|
7
|
+
from .executor import ShellExecutor
|
|
8
|
+
from .groq_client import GroqClient
|
|
9
|
+
from .history import ConversationHistory
|
|
10
|
+
|
|
11
|
+
SYSTEM_PROMPT = """You are Cobalt, an expert Debian Linux assistant and DevOps engineer.
|
|
12
|
+
|
|
13
|
+
Your job is to help the user safely operate the system, inspect files, run commands, and maintain a persistent local conversation.
|
|
14
|
+
|
|
15
|
+
When you need to execute a shell command, read a file, or write to a file, use one of these exact formats in your response:
|
|
16
|
+
- COMMAND: <shell command>
|
|
17
|
+
- READ_FILE: <path>
|
|
18
|
+
- WRITE_FILE: <path>
|
|
19
|
+
<content>
|
|
20
|
+
|
|
21
|
+
If no tool use is required, answer normally.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CobaltAgent:
|
|
26
|
+
def __init__(self, settings: Settings | None = None, history: ConversationHistory | None = None) -> None:
|
|
27
|
+
self.settings = settings or Settings()
|
|
28
|
+
self.history = history or ConversationHistory(self.settings.history_path)
|
|
29
|
+
self.executor = ShellExecutor(self.settings.shell)
|
|
30
|
+
self.client = GroqClient(
|
|
31
|
+
api_key=self.settings.api_key,
|
|
32
|
+
model=self.settings.model,
|
|
33
|
+
base_url=self.settings.base_url,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def _build_messages(self, prompt: str) -> list[dict[str, str]]:
|
|
37
|
+
user_messages = [{"role": "system", "content": SYSTEM_PROMPT}] + self.history.latest(20)
|
|
38
|
+
user_messages.append({"role": "user", "content": prompt})
|
|
39
|
+
return user_messages
|
|
40
|
+
|
|
41
|
+
def _execute_tool(self, tool_name: str, payload: str) -> str:
|
|
42
|
+
tool_name = tool_name.strip().upper()
|
|
43
|
+
if tool_name == "COMMAND":
|
|
44
|
+
result = self.executor.run(payload)
|
|
45
|
+
output = result.stdout.strip() or ""
|
|
46
|
+
error = result.stderr.strip()
|
|
47
|
+
summary = [f"Exit code: {result.returncode}", f"Stdout: {output}", f"Stderr: {error}"]
|
|
48
|
+
return "\n".join(part for part in summary if part)
|
|
49
|
+
if tool_name == "READ_FILE":
|
|
50
|
+
path = Path(payload.strip())
|
|
51
|
+
return path.read_text(encoding="utf-8")
|
|
52
|
+
if tool_name == "WRITE_FILE":
|
|
53
|
+
if "\n" not in payload:
|
|
54
|
+
raise ValueError("WRITE_FILE requires a path and content on separate lines")
|
|
55
|
+
path_text, content = payload.split("\n", 1)
|
|
56
|
+
path = Path(path_text.strip())
|
|
57
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
path.write_text(content, encoding="utf-8")
|
|
59
|
+
return f"Wrote {path}"
|
|
60
|
+
raise ValueError(f"Unsupported tool: {tool_name}")
|
|
61
|
+
|
|
62
|
+
def _extract_tools(self, response: str) -> list[tuple[str, str]]:
|
|
63
|
+
tools: list[tuple[str, str]] = []
|
|
64
|
+
for line in response.splitlines():
|
|
65
|
+
match = re.match(r"^(COMMAND|READ_FILE|WRITE_FILE):\s*(.*)$", line.strip())
|
|
66
|
+
if match:
|
|
67
|
+
tools.append((match.group(1), match.group(2)))
|
|
68
|
+
continue
|
|
69
|
+
if "WRITE_FILE:" in response:
|
|
70
|
+
match = re.search(r"WRITE_FILE:\s*(.+?)\n(.+)", response, re.DOTALL)
|
|
71
|
+
if match:
|
|
72
|
+
path = match.group(1).strip()
|
|
73
|
+
content = match.group(2)
|
|
74
|
+
tools.append(("WRITE_FILE", f"{path}\n{content}"))
|
|
75
|
+
return tools
|
|
76
|
+
|
|
77
|
+
def process(self, prompt: str) -> str:
|
|
78
|
+
if not self.settings.api_key_configured:
|
|
79
|
+
raise RuntimeError("Missing Groq API key. Use the built-in key provided by the package, or set GROQ_API_KEY in a local .env file before running the CLI.")
|
|
80
|
+
|
|
81
|
+
self.history.add("user", prompt)
|
|
82
|
+
messages = self._build_messages(prompt)
|
|
83
|
+
response = self.client.chat_completion(messages, temperature=self.settings.temperature, max_tokens=self.settings.max_tokens)
|
|
84
|
+
|
|
85
|
+
tool_calls = self._extract_tools(response)
|
|
86
|
+
if not tool_calls:
|
|
87
|
+
self.history.add("assistant", response)
|
|
88
|
+
return response
|
|
89
|
+
|
|
90
|
+
tool_results: list[str] = []
|
|
91
|
+
for tool_name, payload in tool_calls:
|
|
92
|
+
try:
|
|
93
|
+
result = self._execute_tool(tool_name, payload)
|
|
94
|
+
tool_results.append(f"{tool_name} result:\n{result}")
|
|
95
|
+
except Exception as exc: # pragma: no cover - defensive path
|
|
96
|
+
tool_results.append(f"{tool_name} error:\n{exc}")
|
|
97
|
+
|
|
98
|
+
final_prompt = "Here are the tool result(s) from my previous action:\n\n" + "\n\n".join(tool_results) + "\n\nPlease provide the final user-facing response based on these results."
|
|
99
|
+
follow_up = self.client.chat_completion(
|
|
100
|
+
[{"role": "system", "content": SYSTEM_PROMPT}] + self.history.latest(20) + [{"role": "user", "content": final_prompt}],
|
|
101
|
+
temperature=self.settings.temperature,
|
|
102
|
+
max_tokens=self.settings.max_tokens,
|
|
103
|
+
)
|
|
104
|
+
self.history.add("assistant", follow_up)
|
|
105
|
+
return follow_up
|
|
106
|
+
|
|
107
|
+
def process_stream(self, prompt: str, on_chunk=None) -> str:
|
|
108
|
+
if not self.settings.api_key_configured:
|
|
109
|
+
raise RuntimeError("Missing Groq API key. Use the built-in key provided by the package, or set GROQ_API_KEY in a local .env file before running the CLI.")
|
|
110
|
+
|
|
111
|
+
if not self.history.messages or self.history.messages[-1].get("role") != "user":
|
|
112
|
+
self.history.add("user", prompt)
|
|
113
|
+
else:
|
|
114
|
+
self.history.messages[-1] = {"role": "user", "content": prompt}
|
|
115
|
+
self.history.save()
|
|
116
|
+
|
|
117
|
+
messages = self._build_messages(prompt)
|
|
118
|
+
buffer = ""
|
|
119
|
+
|
|
120
|
+
if self.history.messages and self.history.messages[-1].get("role") == "assistant":
|
|
121
|
+
self.history.messages[-1]["content"] = ""
|
|
122
|
+
self.history.save()
|
|
123
|
+
else:
|
|
124
|
+
self.history.add("assistant", "")
|
|
125
|
+
|
|
126
|
+
for chunk in self.client.chat_completion_stream(messages, temperature=self.settings.temperature, max_tokens=self.settings.max_tokens):
|
|
127
|
+
buffer += chunk
|
|
128
|
+
if on_chunk is not None:
|
|
129
|
+
on_chunk(chunk)
|
|
130
|
+
|
|
131
|
+
if self.history.messages and self.history.messages[-1].get("role") == "assistant":
|
|
132
|
+
self.history.messages[-1]["content"] = buffer
|
|
133
|
+
self.history.save()
|
|
134
|
+
else:
|
|
135
|
+
self.history.add("assistant", buffer)
|
|
136
|
+
return buffer
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from .agent import CobaltAgent
|
|
8
|
+
from .config import Settings
|
|
9
|
+
from .tui import CobaltTUI
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
13
|
+
parser = argparse.ArgumentParser(description="Cobalt CLI - Debian Linux Groq agent")
|
|
14
|
+
parser.add_argument("prompt", nargs="*", help="Prompt or question to send to the assistant")
|
|
15
|
+
parser.add_argument("--interactive", "-i", action="store_true", help="Launch the full-screen interactive interface")
|
|
16
|
+
parser.add_argument("--api-key", dest="api_key", help="Groq API key; defaults to the built-in package key unless GROQ_API_KEY is set.")
|
|
17
|
+
parser.add_argument("--model", default=None, help="Groq model to use (default: auto, picks the first available model)")
|
|
18
|
+
parser.add_argument("--base-url", default=None, help="Groq API base URL")
|
|
19
|
+
parser.add_argument("--history-path", default=None, help="Path to the conversation history JSON file")
|
|
20
|
+
parser.add_argument("--shell", default=None, help="Shell executable used for commands")
|
|
21
|
+
parser.add_argument("--temperature", type=float, default=None, help="Sampling temperature for model responses")
|
|
22
|
+
parser.add_argument("--max-tokens", type=int, default=None, help="Maximum number of tokens per response")
|
|
23
|
+
return parser
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main(argv: list[str] | None = None) -> int:
|
|
27
|
+
args = build_parser().parse_args(argv)
|
|
28
|
+
settings = Settings(
|
|
29
|
+
api_key=args.api_key or os.getenv("GROQ_API_KEY"),
|
|
30
|
+
model=args.model or os.getenv("GROQ_MODEL", "auto"),
|
|
31
|
+
base_url=args.base_url or os.getenv("GROQ_BASE_URL", "https://api.groq.com/openai/v1"),
|
|
32
|
+
history_path=args.history_path or os.getenv("COBALT_HISTORY_PATH", "~/.cobalt/history.json"),
|
|
33
|
+
shell=args.shell or os.getenv("SHELL", "/bin/bash"),
|
|
34
|
+
temperature=args.temperature if args.temperature is not None else 0.2,
|
|
35
|
+
max_tokens=args.max_tokens if args.max_tokens is not None else 1024,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if args.interactive:
|
|
39
|
+
return CobaltTUI(settings).run()
|
|
40
|
+
|
|
41
|
+
prompt = " ".join(args.prompt)
|
|
42
|
+
if not prompt:
|
|
43
|
+
print("Please provide a prompt or use --interactive for the full-screen UI.", file=sys.stderr)
|
|
44
|
+
return 2
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
agent = CobaltAgent(settings=settings)
|
|
48
|
+
response = agent.process(prompt)
|
|
49
|
+
print(response)
|
|
50
|
+
return 0
|
|
51
|
+
except Exception as exc:
|
|
52
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
53
|
+
return 1
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
DEFAULT_MODEL = "llama-3.3-70b-versatile"
|
|
9
|
+
DEFAULT_BASE_URL = "https://api.groq.com/openai/v1"
|
|
10
|
+
DEFAULT_API_KEY = "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"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _load_env_file() -> None:
|
|
14
|
+
for candidate in (".env", str(Path.home() / ".cobalt" / ".env"), str(Path.home() / ".env")):
|
|
15
|
+
path = Path(candidate).expanduser()
|
|
16
|
+
if not path.exists():
|
|
17
|
+
continue
|
|
18
|
+
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
|
19
|
+
line = raw_line.strip()
|
|
20
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
21
|
+
continue
|
|
22
|
+
key, value = [part.strip() for part in line.split("=", 1)]
|
|
23
|
+
os.environ.setdefault(key, value.strip('"\''))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_load_env_file()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Settings:
|
|
31
|
+
api_key: str | None = None
|
|
32
|
+
model: str = DEFAULT_MODEL
|
|
33
|
+
base_url: str = DEFAULT_BASE_URL
|
|
34
|
+
history_path: str | Path = "~/.cobalt/history.json"
|
|
35
|
+
shell: str = "/bin/bash"
|
|
36
|
+
temperature: float = 0.2
|
|
37
|
+
max_tokens: int = 1024
|
|
38
|
+
|
|
39
|
+
def __post_init__(self) -> None:
|
|
40
|
+
if self.api_key is None:
|
|
41
|
+
self.api_key = os.getenv("GROQ_API_KEY", DEFAULT_API_KEY)
|
|
42
|
+
if not self.model:
|
|
43
|
+
self.model = os.getenv("GROQ_MODEL", DEFAULT_MODEL)
|
|
44
|
+
if not self.base_url:
|
|
45
|
+
self.base_url = os.getenv("GROQ_BASE_URL", DEFAULT_BASE_URL)
|
|
46
|
+
if self.history_path is None:
|
|
47
|
+
self.history_path = os.getenv("COBALT_HISTORY_PATH", str(Path.home() / ".cobalt" / "history.json"))
|
|
48
|
+
self.history_path = Path(self.history_path).expanduser()
|
|
49
|
+
if not self.shell:
|
|
50
|
+
self.shell = os.getenv("SHELL", "/bin/bash")
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def api_key_configured(self) -> bool:
|
|
54
|
+
return bool(self.api_key)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DeepSeekAPIError(RuntimeError):
|
|
10
|
+
"""Raised when the DeepSeek API rejects a request."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DeepSeekClient:
|
|
14
|
+
def __init__(self, api_key: str, model: str = "deepseek-chat", base_url: str = "https://api.deepseek.com") -> None:
|
|
15
|
+
if not api_key:
|
|
16
|
+
raise ValueError("DeepSeek API key is required")
|
|
17
|
+
self.api_key = api_key
|
|
18
|
+
self.model = model
|
|
19
|
+
self.base_url = base_url.rstrip("/")
|
|
20
|
+
|
|
21
|
+
def chat_completion(self, messages: list[dict[str, str]], temperature: float = 0.2, max_tokens: int = 1024) -> str:
|
|
22
|
+
payload = {
|
|
23
|
+
"model": self.model,
|
|
24
|
+
"messages": messages,
|
|
25
|
+
"temperature": temperature,
|
|
26
|
+
"max_tokens": max_tokens,
|
|
27
|
+
}
|
|
28
|
+
headers = {
|
|
29
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
with httpx.Client(timeout=60.0) as client:
|
|
34
|
+
response = client.post(f"{self.base_url}/v1/chat/completions", headers=headers, json=payload)
|
|
35
|
+
|
|
36
|
+
if response.status_code != 200:
|
|
37
|
+
try:
|
|
38
|
+
detail = response.json()
|
|
39
|
+
except ValueError:
|
|
40
|
+
detail = response.text
|
|
41
|
+
raise DeepSeekAPIError(f"DeepSeek API request failed ({response.status_code}): {detail}")
|
|
42
|
+
|
|
43
|
+
data = response.json()
|
|
44
|
+
choices = data.get("choices", [])
|
|
45
|
+
if not choices:
|
|
46
|
+
raise DeepSeekAPIError("DeepSeek API returned no choices")
|
|
47
|
+
message = choices[0].get("message", {})
|
|
48
|
+
content = message.get("content", "")
|
|
49
|
+
if not isinstance(content, str):
|
|
50
|
+
return json.dumps(content)
|
|
51
|
+
return content.strip()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class CommandResult:
|
|
12
|
+
command: str
|
|
13
|
+
returncode: int
|
|
14
|
+
stdout: str
|
|
15
|
+
stderr: str
|
|
16
|
+
duration: float
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def ok(self) -> bool:
|
|
20
|
+
return self.returncode == 0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ShellExecutor:
|
|
24
|
+
"""Execute commands in a POSIX shell environment."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, shell: str = "/bin/bash") -> None:
|
|
27
|
+
self.shell = shell
|
|
28
|
+
|
|
29
|
+
def run(
|
|
30
|
+
self,
|
|
31
|
+
command: str,
|
|
32
|
+
cwd: Optional[str | Path] = None,
|
|
33
|
+
timeout: Optional[float] = None,
|
|
34
|
+
env: Optional[dict[str, str]] = None,
|
|
35
|
+
) -> CommandResult:
|
|
36
|
+
started = time.monotonic()
|
|
37
|
+
completed = subprocess.run(
|
|
38
|
+
command,
|
|
39
|
+
shell=True,
|
|
40
|
+
executable=self.shell,
|
|
41
|
+
cwd=str(cwd) if cwd is not None else None,
|
|
42
|
+
env=env,
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
timeout=timeout,
|
|
46
|
+
check=False,
|
|
47
|
+
)
|
|
48
|
+
duration = time.monotonic() - started
|
|
49
|
+
return CommandResult(
|
|
50
|
+
command=command,
|
|
51
|
+
returncode=completed.returncode,
|
|
52
|
+
stdout=completed.stdout,
|
|
53
|
+
stderr=completed.stderr,
|
|
54
|
+
duration=duration,
|
|
55
|
+
)
|