cobalt-cli-linux 0.1.0__tar.gz → 0.1.1__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 → cobalt_cli_linux-0.1.1}/PKG-INFO +3 -1
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/README.md +2 -0
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/pyproject.toml +4 -1
- cobalt_cli_linux-0.1.1/src/cobalt_cli_linux/ASCII.txt +5 -0
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux/agent.py +50 -20
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux/cli.py +2 -6
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux/groq_client.py +31 -28
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux/tui.py +28 -25
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux.egg-info/PKG-INFO +3 -1
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux.egg-info/SOURCES.txt +1 -0
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/tests/test_cli.py +93 -4
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/setup.cfg +0 -0
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux/__init__.py +0 -0
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux/__main__.py +0 -0
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux/config.py +0 -0
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux/deepseek_client.py +0 -0
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux/executor.py +0 -0
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux/history.py +0 -0
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux.egg-info/dependency_links.txt +0 -0
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux.egg-info/entry_points.txt +0 -0
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux.egg-info/requires.txt +0 -0
- {cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cobalt-cli-linux
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Agentic Groq-powered CLI assistant for Debian Linux
|
|
5
5
|
Author: Cobalt
|
|
6
6
|
License-Expression: MIT
|
|
@@ -35,6 +35,8 @@ Cobalt is an agentic Groq-powered CLI assistant designed for Debian Linux. It ca
|
|
|
35
35
|
- File read/write operations for scriptable agent workflows
|
|
36
36
|
- PyPI-ready package metadata and console script entrypoint
|
|
37
37
|
|
|
38
|
+
When the agent emits a `WRITE_FILE` action, Cobalt creates the requested file using UTF-8 and verifies that the file exists with the expected contents before reporting success.
|
|
39
|
+
|
|
38
40
|
## Installation
|
|
39
41
|
|
|
40
42
|
```bash
|
|
@@ -10,6 +10,8 @@ Cobalt is an agentic Groq-powered CLI assistant designed for Debian Linux. It ca
|
|
|
10
10
|
- File read/write operations for scriptable agent workflows
|
|
11
11
|
- PyPI-ready package metadata and console script entrypoint
|
|
12
12
|
|
|
13
|
+
When the agent emits a `WRITE_FILE` action, Cobalt creates the requested file using UTF-8 and verifies that the file exists with the expected contents before reporting success.
|
|
14
|
+
|
|
13
15
|
## Installation
|
|
14
16
|
|
|
15
17
|
```bash
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cobalt-cli-linux"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.1"
|
|
8
8
|
description = "Agentic Groq-powered CLI assistant for Debian Linux"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -45,5 +45,8 @@ package-dir = {"" = "src"}
|
|
|
45
45
|
[tool.setuptools.packages.find]
|
|
46
46
|
where = ["src"]
|
|
47
47
|
|
|
48
|
+
[tool.setuptools.package-data]
|
|
49
|
+
cobalt_cli_linux = ["ASCII.txt"]
|
|
50
|
+
|
|
48
51
|
[tool.pytest.ini_options]
|
|
49
52
|
pythonpath = ["src"]
|
|
@@ -56,24 +56,49 @@ class CobaltAgent:
|
|
|
56
56
|
path = Path(path_text.strip())
|
|
57
57
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
58
58
|
path.write_text(content, encoding="utf-8")
|
|
59
|
+
if not path.is_file() or path.read_text(encoding="utf-8") != content:
|
|
60
|
+
raise OSError(f"File verification failed for {path}")
|
|
59
61
|
return f"Wrote {path}"
|
|
60
62
|
raise ValueError(f"Unsupported tool: {tool_name}")
|
|
61
63
|
|
|
62
64
|
def _extract_tools(self, response: str) -> list[tuple[str, str]]:
|
|
63
65
|
tools: list[tuple[str, str]] = []
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
lines = response.splitlines()
|
|
67
|
+
index = 0
|
|
68
|
+
while index < len(lines):
|
|
69
|
+
match = re.match(r"^(COMMAND|READ_FILE|WRITE_FILE):\s*(.*)$", lines[index].strip())
|
|
70
|
+
if not match:
|
|
71
|
+
index += 1
|
|
68
72
|
continue
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
|
|
74
|
+
tool_name, payload = match.groups()
|
|
75
|
+
if tool_name == "WRITE_FILE":
|
|
76
|
+
content_lines: list[str] = []
|
|
77
|
+
index += 1
|
|
78
|
+
while index < len(lines) and not re.match(r"^(COMMAND|READ_FILE|WRITE_FILE):\s*", lines[index].strip()):
|
|
79
|
+
content_lines.append(lines[index])
|
|
80
|
+
index += 1
|
|
81
|
+
tools.append((tool_name, f"{payload}\n{chr(10).join(content_lines)}"))
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
tools.append((tool_name, payload))
|
|
85
|
+
index += 1
|
|
75
86
|
return tools
|
|
76
87
|
|
|
88
|
+
def _tool_results(self, response: str) -> list[str]:
|
|
89
|
+
results: list[str] = []
|
|
90
|
+
for tool_name, payload in self._extract_tools(response):
|
|
91
|
+
try:
|
|
92
|
+
result = self._execute_tool(tool_name, payload)
|
|
93
|
+
results.append(f"{tool_name} result:\n{result}")
|
|
94
|
+
except (OSError, ValueError, RuntimeError) as exc: # pragma: no cover - defensive path
|
|
95
|
+
results.append(f"{tool_name} error:\n{exc}")
|
|
96
|
+
return results
|
|
97
|
+
|
|
98
|
+
def _follow_up_messages(self, tool_results: list[str]) -> list[dict[str, str]]:
|
|
99
|
+
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."
|
|
100
|
+
return [{"role": "system", "content": SYSTEM_PROMPT}] + self.history.latest(20) + [{"role": "user", "content": final_prompt}]
|
|
101
|
+
|
|
77
102
|
def process(self, prompt: str) -> str:
|
|
78
103
|
if not self.settings.api_key_configured:
|
|
79
104
|
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.")
|
|
@@ -87,17 +112,9 @@ class CobaltAgent:
|
|
|
87
112
|
self.history.add("assistant", response)
|
|
88
113
|
return response
|
|
89
114
|
|
|
90
|
-
tool_results
|
|
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."
|
|
115
|
+
tool_results = self._tool_results(response)
|
|
99
116
|
follow_up = self.client.chat_completion(
|
|
100
|
-
|
|
117
|
+
self._follow_up_messages(tool_results),
|
|
101
118
|
temperature=self.settings.temperature,
|
|
102
119
|
max_tokens=self.settings.max_tokens,
|
|
103
120
|
)
|
|
@@ -128,6 +145,19 @@ class CobaltAgent:
|
|
|
128
145
|
if on_chunk is not None:
|
|
129
146
|
on_chunk(chunk)
|
|
130
147
|
|
|
148
|
+
tool_results = self._tool_results(buffer)
|
|
149
|
+
if tool_results:
|
|
150
|
+
follow_up_buffer = ""
|
|
151
|
+
for chunk in self.client.chat_completion_stream(
|
|
152
|
+
self._follow_up_messages(tool_results),
|
|
153
|
+
temperature=self.settings.temperature,
|
|
154
|
+
max_tokens=self.settings.max_tokens,
|
|
155
|
+
):
|
|
156
|
+
follow_up_buffer += chunk
|
|
157
|
+
if on_chunk is not None:
|
|
158
|
+
on_chunk(chunk)
|
|
159
|
+
buffer = follow_up_buffer
|
|
160
|
+
|
|
131
161
|
if self.history.messages and self.history.messages[-1].get("role") == "assistant":
|
|
132
162
|
self.history.messages[-1]["content"] = buffer
|
|
133
163
|
self.history.save()
|
|
@@ -35,20 +35,16 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
35
35
|
max_tokens=args.max_tokens if args.max_tokens is not None else 1024,
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
-
if args.interactive:
|
|
38
|
+
if args.interactive or not args.prompt:
|
|
39
39
|
return CobaltTUI(settings).run()
|
|
40
40
|
|
|
41
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
42
|
try:
|
|
47
43
|
agent = CobaltAgent(settings=settings)
|
|
48
44
|
response = agent.process(prompt)
|
|
49
45
|
print(response)
|
|
50
46
|
return 0
|
|
51
|
-
except
|
|
47
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
52
48
|
print(f"Error: {exc}", file=sys.stderr)
|
|
53
49
|
return 1
|
|
54
50
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from typing import ClassVar
|
|
5
6
|
|
|
6
7
|
import httpx
|
|
7
8
|
|
|
@@ -11,7 +12,7 @@ class GroqAPIError(RuntimeError):
|
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class GroqClient:
|
|
14
|
-
DEFAULT_MODELS = [
|
|
15
|
+
DEFAULT_MODELS: ClassVar[list[str]] = [
|
|
15
16
|
"llama-3.3-70b-versatile",
|
|
16
17
|
"llama-3.1-8b-instant",
|
|
17
18
|
"llama-3.3-8b",
|
|
@@ -128,29 +129,31 @@ class GroqClient:
|
|
|
128
129
|
"Content-Type": "application/json",
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
with httpx.Client(timeout=60.0) as client
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
132
|
+
with httpx.Client(timeout=60.0) as client, client.stream(
|
|
133
|
+
"POST", f"{self.base_url}/chat/completions", headers=headers, json=payload
|
|
134
|
+
) as response:
|
|
135
|
+
if response.status_code != 200:
|
|
136
|
+
response.read()
|
|
137
|
+
try:
|
|
138
|
+
detail = response.json()
|
|
139
|
+
except ValueError:
|
|
140
|
+
detail = response.text
|
|
141
|
+
raise GroqAPIError(f"Groq API request failed ({response.status_code}): {detail}")
|
|
142
|
+
|
|
143
|
+
for line in response.iter_lines():
|
|
144
|
+
if not line or not line.startswith("data:"):
|
|
145
|
+
continue
|
|
146
|
+
data = line[5:].strip()
|
|
147
|
+
if data == "[DONE]":
|
|
148
|
+
break
|
|
149
|
+
try:
|
|
150
|
+
event = json.loads(data)
|
|
151
|
+
except json.JSONDecodeError:
|
|
152
|
+
continue
|
|
153
|
+
choices = event.get("choices", [])
|
|
154
|
+
if not choices:
|
|
155
|
+
continue
|
|
156
|
+
delta = choices[0].get("delta", {})
|
|
157
|
+
content = delta.get("content")
|
|
158
|
+
if isinstance(content, str):
|
|
159
|
+
yield content
|
|
@@ -6,7 +6,7 @@ import textwrap
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
from .agent import CobaltAgent
|
|
9
|
-
from .history import ChatStore
|
|
9
|
+
from .history import ChatStore
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class CobaltTUI:
|
|
@@ -25,7 +25,9 @@ class CobaltTUI:
|
|
|
25
25
|
self.ascii_lines = self._load_ascii_art()
|
|
26
26
|
|
|
27
27
|
def _load_ascii_art(self) -> list[str]:
|
|
28
|
-
ascii_path = Path(__file__).
|
|
28
|
+
ascii_path = Path(__file__).with_name("ASCII.txt")
|
|
29
|
+
if not ascii_path.exists():
|
|
30
|
+
ascii_path = Path(__file__).resolve().parents[2] / "ASCII.txt"
|
|
29
31
|
if not ascii_path.exists():
|
|
30
32
|
return ["COBALT"]
|
|
31
33
|
return [line.rstrip() for line in ascii_path.read_text(encoding="utf-8").splitlines() if line.strip()]
|
|
@@ -56,10 +58,8 @@ class CobaltTUI:
|
|
|
56
58
|
title = " ".join(words[:4]).strip()
|
|
57
59
|
return title.title() if title else "New chat"
|
|
58
60
|
|
|
59
|
-
def _should_handle_shortcut(self, ch: int
|
|
60
|
-
|
|
61
|
-
return False
|
|
62
|
-
return ch in (ord("n"), ord("d"))
|
|
61
|
+
def _should_handle_shortcut(self, ch: int) -> bool:
|
|
62
|
+
return ch in (14, 4)
|
|
63
63
|
|
|
64
64
|
def _compose_lines(self, text: str, width: int) -> list[str]:
|
|
65
65
|
lines: list[str] = []
|
|
@@ -67,8 +67,7 @@ class CobaltTUI:
|
|
|
67
67
|
if not paragraph:
|
|
68
68
|
lines.append("")
|
|
69
69
|
continue
|
|
70
|
-
|
|
71
|
-
lines.append(wrapped)
|
|
70
|
+
lines.extend(textwrap.wrap(paragraph, width=max(15, width), break_long_words=False, break_on_hyphens=False))
|
|
72
71
|
return lines
|
|
73
72
|
|
|
74
73
|
def _draw_box(self, stdscr, top: int, left: int, height: int, width: int, title: str | None = None) -> None:
|
|
@@ -89,6 +88,9 @@ class CobaltTUI:
|
|
|
89
88
|
stdscr.bkgd(" ", curses.color_pair(0))
|
|
90
89
|
height, width = stdscr.getmaxyx()
|
|
91
90
|
|
|
91
|
+
header = " Cobalt Code | terminal agent "
|
|
92
|
+
stdscr.addstr(0, 1, header[: max(0, width - 2)], curses.A_BOLD)
|
|
93
|
+
|
|
92
94
|
sidebar_w = min(26, max(20, width // 4))
|
|
93
95
|
sidebar_x = 1
|
|
94
96
|
sidebar_y = 1
|
|
@@ -112,7 +114,7 @@ class CobaltTUI:
|
|
|
112
114
|
main_w = max(20, width - main_x - 2)
|
|
113
115
|
main_y = 1
|
|
114
116
|
main_h = max(8, height - 4)
|
|
115
|
-
self._draw_box(stdscr, main_y, main_x, main_h, main_w, " conversation ")
|
|
117
|
+
self._draw_box(stdscr, main_y, main_x, main_h, main_w, f" {self.history.title or 'conversation'} ")
|
|
116
118
|
|
|
117
119
|
if not self.history.messages:
|
|
118
120
|
for idx, line in enumerate(self.ascii_lines):
|
|
@@ -128,7 +130,7 @@ class CobaltTUI:
|
|
|
128
130
|
for message in self.history.latest(18):
|
|
129
131
|
role = message.get("role", "user").upper()
|
|
130
132
|
content = message.get("content", "")
|
|
131
|
-
prefix = f"
|
|
133
|
+
prefix = f"{role.lower()} "
|
|
132
134
|
for line in self._compose_lines(content, max(12, main_w - 8 - len(prefix))):
|
|
133
135
|
rows.append(f"{prefix}{line}" if prefix and line else line)
|
|
134
136
|
prefix = ""
|
|
@@ -144,9 +146,10 @@ class CobaltTUI:
|
|
|
144
146
|
prompt_x = main_x + max(0, (main_w - len(prompt)) // 2)
|
|
145
147
|
stdscr.addstr(prompt_y, prompt_x, prompt[: max(0, main_w - 2)], curses.A_REVERSE)
|
|
146
148
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
if status:
|
|
150
|
+
status_line = status[: max(0, width - 1)]
|
|
151
|
+
stdscr.addstr(height - 1, 0, status_line)
|
|
152
|
+
stdscr.clrtoeol()
|
|
150
153
|
stdscr.refresh()
|
|
151
154
|
|
|
152
155
|
def _stream_response(self, stdscr, input_text: str) -> str:
|
|
@@ -158,7 +161,7 @@ class CobaltTUI:
|
|
|
158
161
|
if self.history.messages and self.history.messages[-1]["role"] == "assistant":
|
|
159
162
|
self.history.messages[-1]["content"] = buffer
|
|
160
163
|
self.history.save()
|
|
161
|
-
self._render(stdscr, input_text, "
|
|
164
|
+
self._render(stdscr, input_text, "")
|
|
162
165
|
|
|
163
166
|
agent = CobaltAgent(settings=self.settings)
|
|
164
167
|
agent.history = self.history
|
|
@@ -199,7 +202,7 @@ class CobaltTUI:
|
|
|
199
202
|
curses.noecho()
|
|
200
203
|
curses.mousemask(curses.BUTTON1_PRESSED | curses.BUTTON1_RELEASED | curses.BUTTON1_CLICKED)
|
|
201
204
|
input_text = ""
|
|
202
|
-
status = "
|
|
205
|
+
status = ""
|
|
203
206
|
|
|
204
207
|
while True:
|
|
205
208
|
self._render(stdscr, input_text, status)
|
|
@@ -207,16 +210,15 @@ class CobaltTUI:
|
|
|
207
210
|
|
|
208
211
|
if ch == curses.KEY_MOUSE:
|
|
209
212
|
_, x, y, _, button_state = curses.getmouse()
|
|
210
|
-
if button_state & (curses.BUTTON1_PRESSED | curses.BUTTON1_CLICKED | curses.BUTTON1_RELEASED):
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
input_text = ""
|
|
213
|
+
if button_state & (curses.BUTTON1_PRESSED | curses.BUTTON1_CLICKED | curses.BUTTON1_RELEASED) and self._handle_mouse_click(x, y):
|
|
214
|
+
status = "Chat selected"
|
|
215
|
+
input_text = ""
|
|
214
216
|
continue
|
|
215
217
|
|
|
216
|
-
if ch
|
|
218
|
+
if ch == 3:
|
|
217
219
|
break
|
|
218
|
-
if
|
|
219
|
-
if ch ==
|
|
220
|
+
if self._should_handle_shortcut(ch):
|
|
221
|
+
if ch == 14:
|
|
220
222
|
new_chat = self.chat_store.new_chat(title="New chat")
|
|
221
223
|
self.current_chat = new_chat
|
|
222
224
|
self.history = self.current_chat
|
|
@@ -226,7 +228,7 @@ class CobaltTUI:
|
|
|
226
228
|
status = "New chat started"
|
|
227
229
|
continue
|
|
228
230
|
|
|
229
|
-
if ch ==
|
|
231
|
+
if ch == 4 and self.selected_chat_path:
|
|
230
232
|
target = Path(self.selected_chat_path)
|
|
231
233
|
if target.exists():
|
|
232
234
|
self.chat_store.delete_chat(target)
|
|
@@ -250,8 +252,9 @@ class CobaltTUI:
|
|
|
250
252
|
self.history.save()
|
|
251
253
|
self._stream_response(stdscr, input_text)
|
|
252
254
|
status = "Response received"
|
|
253
|
-
except
|
|
254
|
-
|
|
255
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
256
|
+
self.history.add("assistant", f"Error: {exc}")
|
|
257
|
+
status = ""
|
|
255
258
|
input_text = ""
|
|
256
259
|
continue
|
|
257
260
|
if ch in (curses.KEY_BACKSPACE, 127, 8):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cobalt-cli-linux
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Agentic Groq-powered CLI assistant for Debian Linux
|
|
5
5
|
Author: Cobalt
|
|
6
6
|
License-Expression: MIT
|
|
@@ -35,6 +35,8 @@ Cobalt is an agentic Groq-powered CLI assistant designed for Debian Linux. It ca
|
|
|
35
35
|
- File read/write operations for scriptable agent workflows
|
|
36
36
|
- PyPI-ready package metadata and console script entrypoint
|
|
37
37
|
|
|
38
|
+
When the agent emits a `WRITE_FILE` action, Cobalt creates the requested file using UTF-8 and verifies that the file exists with the expected contents before reporting success.
|
|
39
|
+
|
|
38
40
|
## Installation
|
|
39
41
|
|
|
40
42
|
```bash
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
+
from cobalt_cli_linux import cli
|
|
4
|
+
from cobalt_cli_linux.agent import CobaltAgent
|
|
3
5
|
from cobalt_cli_linux.config import Settings
|
|
4
6
|
from cobalt_cli_linux.executor import ShellExecutor
|
|
5
7
|
from cobalt_cli_linux.groq_client import GroqClient
|
|
@@ -47,10 +49,97 @@ def test_chat_store_delete_chat(tmp_path: Path) -> None:
|
|
|
47
49
|
def test_tui_shortcuts_ignore_typed_letters() -> None:
|
|
48
50
|
tui = CobaltTUI(settings=Settings())
|
|
49
51
|
|
|
50
|
-
assert tui._should_handle_shortcut(
|
|
51
|
-
assert tui._should_handle_shortcut(
|
|
52
|
-
assert tui._should_handle_shortcut(ord("
|
|
53
|
-
assert tui._should_handle_shortcut(ord("
|
|
52
|
+
assert tui._should_handle_shortcut(14) is True
|
|
53
|
+
assert tui._should_handle_shortcut(4) is True
|
|
54
|
+
assert tui._should_handle_shortcut(ord("q")) is False
|
|
55
|
+
assert tui._should_handle_shortcut(ord("n")) is False
|
|
56
|
+
assert tui._should_handle_shortcut(ord("d")) is False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_q_is_not_a_shortcut_when_typing() -> None:
|
|
60
|
+
tui = CobaltTUI(settings=Settings())
|
|
61
|
+
|
|
62
|
+
assert tui._should_handle_shortcut(ord("q")) is False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_no_prompt_launches_tui(monkeypatch) -> None:
|
|
66
|
+
launched = []
|
|
67
|
+
|
|
68
|
+
class FakeTUI:
|
|
69
|
+
def __init__(self, settings):
|
|
70
|
+
launched.append(settings)
|
|
71
|
+
|
|
72
|
+
def run(self):
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
monkeypatch.setattr(cli, "CobaltTUI", FakeTUI)
|
|
76
|
+
|
|
77
|
+
assert cli.main([]) == 0
|
|
78
|
+
assert launched
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_stream_error_reads_response_before_inspecting_body(monkeypatch) -> None:
|
|
82
|
+
class FakeResponse:
|
|
83
|
+
status_code = 401
|
|
84
|
+
text = "unauthorized"
|
|
85
|
+
was_read = False
|
|
86
|
+
|
|
87
|
+
def read(self):
|
|
88
|
+
self.was_read = True
|
|
89
|
+
|
|
90
|
+
def json(self):
|
|
91
|
+
assert self.was_read is True
|
|
92
|
+
return {"error": {"message": "unauthorized"}}
|
|
93
|
+
|
|
94
|
+
def __enter__(self):
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def __exit__(self, exc_type, exc, tb):
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
class FakeStreamClient:
|
|
101
|
+
def __init__(self, timeout=None):
|
|
102
|
+
self.response = FakeResponse()
|
|
103
|
+
|
|
104
|
+
def __enter__(self):
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def __exit__(self, exc_type, exc, tb):
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
def get(self, url, headers=None):
|
|
111
|
+
return type("Response", (), {"status_code": 200, "json": lambda self: {"data": []}})()
|
|
112
|
+
|
|
113
|
+
def stream(self, method, url, headers=None, json=None):
|
|
114
|
+
return self.response
|
|
115
|
+
|
|
116
|
+
monkeypatch.setattr("cobalt_cli_linux.groq_client.httpx.Client", FakeStreamClient)
|
|
117
|
+
client = GroqClient("test-key", model="llama-3.1-8b-instant")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
next(client.chat_completion_stream([{"role": "user", "content": "hello"}]))
|
|
121
|
+
except RuntimeError as exc:
|
|
122
|
+
assert "401" in str(exc)
|
|
123
|
+
else:
|
|
124
|
+
raise AssertionError("Expected streamed API error")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_write_file_tool_creates_and_verifies_file(tmp_path: Path) -> None:
|
|
128
|
+
agent = CobaltAgent(settings=Settings(), history=ConversationHistory(tmp_path / "history.json"))
|
|
129
|
+
target = tmp_path / "test.txt"
|
|
130
|
+
|
|
131
|
+
result = agent._execute_tool("WRITE_FILE", f"{target}\nhello")
|
|
132
|
+
|
|
133
|
+
assert result == f"Wrote {target}"
|
|
134
|
+
assert target.read_text(encoding="utf-8") == "hello"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_write_file_directive_is_parsed_once() -> None:
|
|
138
|
+
agent = CobaltAgent(settings=Settings())
|
|
139
|
+
|
|
140
|
+
tools = agent._extract_tools("WRITE_FILE: test.txt\nhello\nworld")
|
|
141
|
+
|
|
142
|
+
assert tools == [("WRITE_FILE", "test.txt\nhello\nworld")]
|
|
54
143
|
|
|
55
144
|
|
|
56
145
|
def test_shell_executor_runs_command() -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux.egg-info/requires.txt
RENAMED
|
File without changes
|
{cobalt_cli_linux-0.1.0 → cobalt_cli_linux-0.1.1}/src/cobalt_cli_linux.egg-info/top_level.txt
RENAMED
|
File without changes
|