cycls 0.0.2.79__py3-none-any.whl → 0.0.2.81__py3-none-any.whl
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.
- cycls/chat.py +104 -0
- cycls/default-theme/assets/{index-D5EDcI4J.js → index-C2r4Daz3.js} +86 -73
- cycls/default-theme/assets/{index-B0ZKcm_V.css → index-DWGS8zpa.css} +1 -1
- cycls/default-theme/index.html +2 -2
- cycls/grpc/client.py +1 -1
- cycls/runtime.py +14 -7
- cycls/sdk.py +9 -4
- cycls/web.py +6 -2
- {cycls-0.0.2.79.dist-info → cycls-0.0.2.81.dist-info}/METADATA +3 -1
- cycls-0.0.2.81.dist-info/RECORD +20 -0
- cycls-0.0.2.81.dist-info/entry_points.txt +3 -0
- cycls/cli.py +0 -217
- cycls-0.0.2.79.dist-info/RECORD +0 -20
- cycls-0.0.2.79.dist-info/entry_points.txt +0 -3
- {cycls-0.0.2.79.dist-info → cycls-0.0.2.81.dist-info}/WHEEL +0 -0
cycls/chat.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""cycls chat - Claude Code style CLI for cycls agents"""
|
|
3
|
+
|
|
4
|
+
import json, os, re, sys
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
RESET, BOLD, DIM = "\033[0m", "\033[1m", "\033[2m"
|
|
8
|
+
BLUE, GREEN, YELLOW, RED = "\033[34m", "\033[32m", "\033[33m", "\033[31m"
|
|
9
|
+
CALLOUTS = {"success": ("✓", GREEN), "warning": ("⚠", YELLOW), "info": ("ℹ", BLUE), "error": ("✗", RED)}
|
|
10
|
+
|
|
11
|
+
separator = lambda: f"{DIM}{'─' * min(os.get_terminal_size().columns if sys.stdout.isatty() else 80, 80)}{RESET}"
|
|
12
|
+
markdown = lambda text: re.sub(r"\*\*(.+?)\*\*", f"{BOLD}\\1{RESET}", text)
|
|
13
|
+
header = lambda title, meta, color=GREEN, dim=False: print(f"{color}●{RESET} {BOLD}{title}{RESET}\n ⎿ {meta}{DIM if dim else ''}", flush=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def table(headers, rows):
|
|
17
|
+
if not headers: return
|
|
18
|
+
widths = [max(len(str(h)), *(len(str(r[i])) for r in rows if i < len(r))) for i, h in enumerate(headers)]
|
|
19
|
+
line = lambda left, mid, right: left + mid.join("─" * (w + 2) for w in widths) + right
|
|
20
|
+
row = lambda cells, bold=False: "│" + "│".join(f" {BOLD if bold else ''}{str(cells[i] if i < len(cells) else '').ljust(widths[i])}{RESET if bold else ''} " for i in range(len(widths))) + "│"
|
|
21
|
+
print(f"{line('┌', '┬', '┐')}\n{row(headers, True)}\n{line('├', '┼', '┤')}")
|
|
22
|
+
for r in rows: print(row(r))
|
|
23
|
+
print(line("└", "┴", "┘"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def chat(url):
|
|
27
|
+
messages, endpoint = [], f"{url.rstrip('/')}/chat/cycls"
|
|
28
|
+
print(f"\n{BOLD}cycls{RESET} {DIM}|{RESET} {url}\n")
|
|
29
|
+
|
|
30
|
+
while True:
|
|
31
|
+
try:
|
|
32
|
+
print(separator())
|
|
33
|
+
user_input = input(f"{BOLD}{BLUE}❯{RESET} ").strip()
|
|
34
|
+
print(separator())
|
|
35
|
+
|
|
36
|
+
if not user_input: continue
|
|
37
|
+
if user_input in ("/q", "exit", "quit"): break
|
|
38
|
+
if user_input == "/c": messages, _ = [], print(f"{GREEN}⏺ Cleared{RESET}"); continue
|
|
39
|
+
|
|
40
|
+
messages.append({"role": "user", "content": user_input})
|
|
41
|
+
block, tbl = None, ([], [])
|
|
42
|
+
|
|
43
|
+
def close():
|
|
44
|
+
nonlocal block, tbl
|
|
45
|
+
if block == "thinking": print(RESET)
|
|
46
|
+
if block == "text": print()
|
|
47
|
+
if block == "table" and tbl[0]: table(*tbl); tbl = ([], [])
|
|
48
|
+
if block: print()
|
|
49
|
+
block = None
|
|
50
|
+
|
|
51
|
+
with httpx.stream("POST", endpoint, json={"messages": messages}, timeout=None) as response:
|
|
52
|
+
for line in response.iter_lines():
|
|
53
|
+
if not line.startswith("data: ") or line == "data: [DONE]": continue
|
|
54
|
+
data = json.loads(line[6:])
|
|
55
|
+
type = data.get("type")
|
|
56
|
+
|
|
57
|
+
if type is None:
|
|
58
|
+
print(markdown(data if isinstance(data, str) else data.get("text", "")), end="", flush=True); continue
|
|
59
|
+
|
|
60
|
+
if type != block: close()
|
|
61
|
+
|
|
62
|
+
if type in ("thinking", "text"):
|
|
63
|
+
if block != type: header(type.capitalize(), "Live", dim=(type == "thinking")); block = type
|
|
64
|
+
print((markdown if type == "text" else str)(data.get(type, "")), end="", flush=True)
|
|
65
|
+
elif type == "code":
|
|
66
|
+
code = data.get("code", ""); header(f"Code({data.get('language', '')})", f"{code.count(chr(10))+1} lines"); print(code, flush=True); block = type
|
|
67
|
+
elif type == "status":
|
|
68
|
+
print(f"{DIM}[{data.get('status', '')}]{RESET} ", end="", flush=True)
|
|
69
|
+
elif type == "table":
|
|
70
|
+
if "headers" in data:
|
|
71
|
+
if tbl[0]: table(*tbl)
|
|
72
|
+
header("Table", f"{len(data['headers'])} cols"); tbl, block = (data["headers"], []), type
|
|
73
|
+
elif "row" in data: tbl[1].append(data["row"])
|
|
74
|
+
elif type == "callout":
|
|
75
|
+
style = data.get("style", "info"); icon, color = CALLOUTS.get(style, ("•", RESET))
|
|
76
|
+
header(style.capitalize(), f"{icon} {data.get('callout', '')}", color=color); block = type
|
|
77
|
+
elif type == "image":
|
|
78
|
+
header("Image", data.get("src", "")); block = type
|
|
79
|
+
|
|
80
|
+
close()
|
|
81
|
+
|
|
82
|
+
except KeyboardInterrupt: print()
|
|
83
|
+
except EOFError: break
|
|
84
|
+
except (httpx.ReadError, httpx.ConnectError) as e: print(f"{RED}⏺ Connection error: {e}{RESET}"); messages and messages.pop()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main():
|
|
88
|
+
if len(sys.argv) < 2:
|
|
89
|
+
print("Usage: cycls chat <url|port>")
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
arg = sys.argv[1]
|
|
92
|
+
if arg.isdigit():
|
|
93
|
+
port = int(arg)
|
|
94
|
+
if not (1 <= port <= 65535):
|
|
95
|
+
print(f"Error: Invalid port {port}. Must be between 1 and 65535.")
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
url = f"http://localhost:{port}"
|
|
98
|
+
else:
|
|
99
|
+
url = arg
|
|
100
|
+
chat(url)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
main()
|