v0.2.0 42a5079

zut_client.py RawHistoryFolder

1"""Minimal Python client for the `zut rpc` JSON protocol.
2
3Usage:
4    python zut_client.py "fix the failing test"
5
6Spawns `zut rpc`, sends one prompt, prints assistant text as it streams,
7and exits when the turn finishes. No external dependencies — stdlib
8only. Implements just enough of the protocol to be useful as a
9starting point; see docs/rpc.md for the full schema.
10"""
11from __future__ import annotations
12
13import json
14import os
15import subprocess
16import sys
17import threading
18import uuid
19
20
21class ZutClient:
22    def __init__(self, *flags: str) -> None:
23        argv = ["zut", "rpc", *flags]
24        env = os.environ.copy()
25        self.proc = subprocess.Popen(
26            argv,
27            stdin=subprocess.PIPE,
28            stdout=subprocess.PIPE,
29            stderr=sys.stderr,
30            env=env,
31            text=True,
32            bufsize=1,  # line-buffered
33        )
34        self._lock = threading.Lock()
35
36    def send(self, **command) -> str:
37        """Send one command, return its `id`."""
38        if "id" not in command:
39            command["id"] = uuid.uuid4().hex[:8]
40        line = json.dumps(command)
41        with self._lock:
42            assert self.proc.stdin is not None
43            self.proc.stdin.write(line + "\n")
44            self.proc.stdin.flush()
45        return command["id"]
46
47    def events(self):
48        """Yield every JSON object the server emits, until stdout closes."""
49        assert self.proc.stdout is not None
50        for line in self.proc.stdout:
51            line = line.strip()
52            if not line:
53                continue
54            try:
55                yield json.loads(line)
56            except json.JSONDecodeError:
57                continue
58
59    def close(self) -> None:
60        if self.proc.stdin is not None:
61            try:
62                self.proc.stdin.close()
63            except Exception:
64                pass
65        self.proc.wait(timeout=5)
66
67
68def main() -> int:
69    if len(sys.argv) < 2:
70        print("usage: zut_client.py <prompt>", file=sys.stderr)
71        return 2
72    prompt = " ".join(sys.argv[1:])
73
74    client = ZutClient()
75    try:
76        token = os.environ.get("ZUTCORE_RPC_TOKEN")
77        if token:
78            client.send(type="hello", token=token)
79
80        client.send(type="prompt", message=prompt)
81
82        for ev in client.events():
83            t = ev.get("type")
84            if t == "text_delta":
85                sys.stdout.write(ev.get("delta", ""))
86                sys.stdout.flush()
87            elif t == "tool_call":
88                print(
89                    f"\n[tool] {ev.get('name')}({json.dumps(ev.get('args', {}))})",
90                    file=sys.stderr,
91                )
92            elif t == "tool_result":
93                if ev.get("is_error"):
94                    print("[tool error]", file=sys.stderr)
95            elif t == "usage":
96                cum = ev.get("cumulative", {})
97                print(
98                    f"\n[usage] cum input={cum.get('input')} "
99                    f"output={cum.get('output')} cost=${cum.get('cost_usd', 0):.4f}",
100                    file=sys.stderr,
101                )
102            elif t == "done":
103                break
104            elif t == "error":
105                print(f"\n[error] {ev.get('message')}", file=sys.stderr)
106                return 1
107        print()
108        return 0
109    finally:
110        client.close()
111
112
113if __name__ == "__main__":
114    sys.exit(main())

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close