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 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()