Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"plugins": [
{
"name": "kbagent",
"version": "0.77.1",
"version": "0.78.0",
"source": "./plugins/kbagent",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"category": "development"
Expand Down
2 changes: 1 addition & 1 deletion plugins/kbagent/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kbagent",
"version": "0.77.1",
"version": "0.78.0",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"author": {
"name": "Keboola",
Expand Down
4 changes: 2 additions & 2 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -2138,7 +2138,7 @@ transparent -- no user action is normally required.
- Never crashes the CLI -- update failures leave the current invocation running
and print a recovery command (since v0.76.2)

### Windows updates are deferred, not immediate (since v0.77.1)
### Windows updates are deferred, not immediate (since v0.78.0)

`uv tool install` recreates a tool environment by **removing** it and then
building a fresh venv at the same path. It is not atomic and has no rollback.
Expand All @@ -2164,7 +2164,7 @@ So on Windows kbagent never installs into its own live environment:
- `KBAGENT_DEFER_UPDATE=1` / `=0` forces the deferred path on or off,
overriding the platform default.

A slow install is never killed on any platform (also since v0.77.1): the
A slow install is never killed on any platform (also since v0.78.0): the
timeout bounds only how long kbagent waits, because terminating uv mid-write
produces the same half-deleted environment a file lock does. When that happens
the banner says the install is *still running* and deliberately offers no
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "keboola-cli"
version = "0.77.1"
version = "0.78.0"
description = "AI-friendly CLI for managing Keboola projects"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
10 changes: 9 additions & 1 deletion src/keboola_agent_cli/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@

# Ordered newest-first. Each value is a list of brief one-line descriptions.
CHANGELOG: dict[str, list[str]] = {
"0.77.1": [
"0.78.0": [
"Fix (#546): `kbagent --json` no longer crashes with `UnicodeEncodeError` on Windows "
"consoles using a non-UTF-8 codepage (cp1250 on Czech/Polish/Hungarian Windows). Any "
"non-ASCII character in the data -- an arrow in a flow name was the report -- made "
"machine-readable output unusable, because pydantic's `model_dump_json` emits raw UTF-8 "
"and `sys.stdout` then encoded it through the console codepage. All machine output, "
"including the `--stream` NDJSON from `kbagent agent run`, is now written as UTF-8 bytes "
"independent of the console. The `PYTHONUTF8=1` workaround is no longer needed. JSON "
"lines now end LF rather than CRLF on Windows. Thanks to @MichalProchazka for the report.",
Comment thread
padak marked this conversation as resolved.
"Fix (#528): the Windows self-update no longer corrupts the uv tool environment. "
"`uv tool install` recreates a tool environment by REMOVING it and then building a fresh "
"venv at the same path -- it is not atomic and has no rollback. On POSIX that is harmless, "
Expand Down
53 changes: 33 additions & 20 deletions tests/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import json
import sys
from collections.abc import Callable
from dataclasses import dataclass
from io import StringIO
from typing import cast

Expand Down Expand Up @@ -1080,6 +1081,23 @@ def test_falls_back_to_csv_preview_for_full_export(self) -> None:
assert "alice" in output


@dataclass(frozen=True)
class Cp1250Stdout:
"""A stdout whose text layer cannot encode non-ASCII, like a cp1250 console.

`raw` is held directly rather than reached through `text.buffer`, whose
declared type (`_WrappedBuffer`) has no `getvalue`.
"""

text: io.TextIOWrapper
raw: io.BytesIO

@classmethod
def create(cls) -> "Cp1250Stdout":
raw = io.BytesIO()
return cls(io.TextIOWrapper(raw, encoding="cp1250", newline=""), raw)


class TestMachineOutputIsAlwaysUtf8:
"""`--json` must not depend on the console codepage (issue #546).

Expand All @@ -1097,17 +1115,12 @@ class TestMachineOutputIsAlwaysUtf8:

ARROW_NAME = "extract → transform"

@staticmethod
def _cp1250_stdout() -> io.TextIOWrapper:
"""A stdout whose text layer cannot encode the payload, like cp1250."""
return io.TextIOWrapper(io.BytesIO(), encoding="cp1250", newline="")

def _capture(self, monkeypatch: pytest.MonkeyPatch, action: Callable[[], None]) -> bytes:
stream = self._cp1250_stdout()
monkeypatch.setattr(sys, "stdout", stream)
stdout = Cp1250Stdout.create()
monkeypatch.setattr(sys, "stdout", stdout.text)
action()
stream.flush()
return cast(io.BytesIO, stream.buffer).getvalue()
stdout.text.flush()
return stdout.raw.getvalue()

def test_output_survives_a_codepage_that_cannot_encode_the_data(
self, monkeypatch: pytest.MonkeyPatch
Expand Down Expand Up @@ -1155,14 +1168,14 @@ def test_human_mode_output_written_earlier_keeps_its_place(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Flushing the text layer first stops the two writers reordering."""
stream = self._cp1250_stdout()
monkeypatch.setattr(sys, "stdout", stream)
stdout = Cp1250Stdout.create()
monkeypatch.setattr(sys, "stdout", stdout.text)

sys.stdout.write("first\n")
write_machine_output("second")
stream.flush()
stdout.text.flush()

assert cast(io.BytesIO, stream.buffer).getvalue() == b"first\nsecond\n"
assert stdout.raw.getvalue() == b"first\nsecond\n"


class TestStreamedAgentEventsAreUtf8:
Expand All @@ -1180,14 +1193,14 @@ def test_event_with_non_ascii_does_not_crash_on_a_cp1250_console(
) -> None:
from keboola_agent_cli.commands.agent import _render_stream_event

stream = io.TextIOWrapper(io.BytesIO(), encoding="cp1250", newline="")
monkeypatch.setattr(sys, "stdout", stream)
stdout = Cp1250Stdout.create()
monkeypatch.setattr(sys, "stdout", stdout.text)
formatter = OutputFormatter(json_mode=True)

_render_stream_event(formatter, {"event": "log", "data": {"msg": "načítám → hotovo"}})
stream.flush()
stdout.text.flush()

written = cast(io.BytesIO, stream.buffer).getvalue()
written = stdout.raw.getvalue()
assert json.loads(written.decode("utf-8"))["data"]["msg"] == "načítám → hotovo"

def test_each_event_is_flushed_so_consumers_see_it_immediately(
Expand All @@ -1196,11 +1209,11 @@ def test_each_event_is_flushed_so_consumers_see_it_immediately(
"""A stream consumer reads line by line; buffering would stall it."""
from keboola_agent_cli.commands.agent import _render_stream_event

stream = io.TextIOWrapper(io.BytesIO(), encoding="cp1250", newline="")
monkeypatch.setattr(sys, "stdout", stream)
stdout = Cp1250Stdout.create()
monkeypatch.setattr(sys, "stdout", stdout.text)
formatter = OutputFormatter(json_mode=True)

_render_stream_event(formatter, {"event": "init", "data": {}})

# Readable without an explicit flush by the test.
assert cast(io.BytesIO, stream.buffer).getvalue().endswith(b"\n")
assert stdout.raw.getvalue().endswith(b"\n")
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading