cycls 0.0.2.70__py3-none-any.whl → 0.0.2.71__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/cli.py ADDED
@@ -0,0 +1,217 @@
1
+ import sys
2
+ import json
3
+ import time
4
+ import threading
5
+ import httpx
6
+
7
+ # ANSI codes
8
+ DIM = "\033[2m"
9
+ BOLD = "\033[1m"
10
+ RESET = "\033[0m"
11
+ CLEAR_LINE = "\r\033[K"
12
+ GREEN = "\033[32m"
13
+ YELLOW = "\033[33m"
14
+ BLUE = "\033[34m"
15
+ RED = "\033[31m"
16
+ CYAN = "\033[36m"
17
+ MAGENTA = "\033[35m"
18
+
19
+ CALLOUT_STYLES = {
20
+ "success": ("✓", GREEN),
21
+ "warning": ("⚠", YELLOW),
22
+ "info": ("ℹ", BLUE),
23
+ "error": ("✗", RED),
24
+ }
25
+
26
+ def format_time(seconds):
27
+ if seconds < 60:
28
+ return f"{seconds:.1f}s"
29
+ return f"{int(seconds // 60)}m {int(seconds % 60)}s"
30
+
31
+ def render_table(headers, rows):
32
+ if not headers:
33
+ return
34
+ widths = [len(str(h)) for h in headers]
35
+ for row in rows:
36
+ for i, cell in enumerate(row):
37
+ if i < len(widths):
38
+ widths[i] = max(widths[i], len(str(cell)))
39
+
40
+ top = "┌" + "┬".join("─" * (w + 2) for w in widths) + "┐"
41
+ sep = "├" + "┼".join("─" * (w + 2) for w in widths) + "┤"
42
+ bot = "└" + "┴".join("─" * (w + 2) for w in widths) + "┘"
43
+
44
+ def fmt_row(cells, bold=False):
45
+ parts = []
46
+ for i, w in enumerate(widths):
47
+ cell = str(cells[i]) if i < len(cells) else ""
48
+ if bold:
49
+ parts.append(f" {BOLD}{cell.ljust(w)}{RESET} ")
50
+ else:
51
+ parts.append(f" {cell.ljust(w)} ")
52
+ return "│" + "│".join(parts) + "│"
53
+
54
+ print(top)
55
+ print(fmt_row(headers, bold=True))
56
+ print(sep)
57
+ for row in rows:
58
+ print(fmt_row(row))
59
+ print(bot)
60
+
61
+ class Spinner:
62
+ def __init__(self):
63
+ self.active = False
64
+ self.thread = None
65
+ self.frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
66
+
67
+ def start(self):
68
+ self.active = True
69
+ def spin():
70
+ i = 0
71
+ while self.active:
72
+ if not self.active:
73
+ break
74
+ print(f"\r{MAGENTA}{self.frames[i % len(self.frames)]}{RESET} ", end="", flush=True)
75
+ for _ in range(8): # Check active more frequently
76
+ if not self.active:
77
+ break
78
+ time.sleep(0.01)
79
+ i += 1
80
+ self.thread = threading.Thread(target=spin, daemon=True)
81
+ self.thread.start()
82
+
83
+ def stop(self):
84
+ self.active = False
85
+ if self.thread:
86
+ self.thread.join(timeout=0.2)
87
+ print(f"{CLEAR_LINE}", end="", flush=True)
88
+
89
+ def chat(url):
90
+ messages = []
91
+ endpoint = f"{url.rstrip('/')}/chat/cycls"
92
+
93
+ print(f"\n{MAGENTA}●{RESET} {BOLD}{url}{RESET}\n")
94
+
95
+ while True:
96
+ try:
97
+ user_input = input(f"{CYAN}❯{RESET} ")
98
+ if not user_input.strip():
99
+ continue
100
+
101
+ messages.append({"role": "user", "content": user_input})
102
+ print()
103
+
104
+ start_time = time.time()
105
+ in_thinking = False
106
+ table_headers = []
107
+ table_rows = []
108
+
109
+ with httpx.stream("POST", endpoint, json={"messages": messages}, timeout=None) as r:
110
+ for line in r.iter_lines():
111
+ if line.startswith("data: ") and line != "data: [DONE]":
112
+
113
+ data = json.loads(line[6:])
114
+ msg_type = data.get("type")
115
+
116
+ # Handle plain string or missing type
117
+ if msg_type is None:
118
+ # Could be OpenAI format or plain text
119
+ if isinstance(data, str):
120
+ print(data, end="", flush=True)
121
+ continue
122
+ elif "choices" in data:
123
+ # OpenAI format
124
+ content = data.get("choices", [{}])[0].get("delta", {}).get("content", "")
125
+ if content:
126
+ print(content, end="", flush=True)
127
+ continue
128
+ elif "text" in data:
129
+ print(data.get("text", ""), end="", flush=True)
130
+ continue
131
+ elif "content" in data:
132
+ print(data.get("content", ""), end="", flush=True)
133
+ continue
134
+ else:
135
+ # Debug: print raw data
136
+ print(f"[debug: {data}]", end="", flush=True)
137
+ continue
138
+
139
+ # Close thinking if switching
140
+ if msg_type != "thinking" and in_thinking:
141
+ print(f"</thinking>{RESET}\n", end="", flush=True)
142
+ in_thinking = False
143
+
144
+ # Flush table if switching
145
+ if msg_type != "table" and table_headers:
146
+ render_table(table_headers, table_rows)
147
+ table_headers = []
148
+ table_rows = []
149
+
150
+ if msg_type == "thinking":
151
+ if not in_thinking:
152
+ print(f"{DIM}<thinking>", end="", flush=True)
153
+ in_thinking = True
154
+ print(data.get("thinking", ""), end="", flush=True)
155
+
156
+ elif msg_type == "text":
157
+ print(data.get("text", ""), end="", flush=True)
158
+
159
+ elif msg_type == "code":
160
+ lang = data.get("language", "")
161
+ print(f"\n```{lang}\n{data.get('code', '')}\n```\n", end="", flush=True)
162
+
163
+ elif msg_type == "status":
164
+ print(f"{DIM}[{data.get('status', '')}]{RESET} ", end="", flush=True)
165
+
166
+ elif msg_type == "table":
167
+ if "headers" in data:
168
+ if table_headers:
169
+ render_table(table_headers, table_rows)
170
+ table_headers = data["headers"]
171
+ table_rows = []
172
+ elif "row" in data:
173
+ table_rows.append(data["row"])
174
+
175
+ elif msg_type == "callout":
176
+ style = data.get("style", "info")
177
+ icon, color = CALLOUT_STYLES.get(style, ("•", RESET))
178
+ title = data.get("title", "")
179
+ text = data.get("callout", "")
180
+ if title:
181
+ print(f"\n{color}{icon} {BOLD}{title}{RESET}")
182
+ print(f"{color} {text}{RESET}\n", end="", flush=True)
183
+ else:
184
+ print(f"\n{color}{icon} {text}{RESET}\n", end="", flush=True)
185
+
186
+ elif msg_type == "image":
187
+ print(f"{DIM}[image: {data.get('src', '')}]{RESET}", end="", flush=True)
188
+
189
+ # Flush remaining
190
+ if table_headers:
191
+ render_table(table_headers, table_rows)
192
+ if in_thinking:
193
+ print(f"</thinking>{RESET}", end="", flush=True)
194
+
195
+ elapsed = time.time() - start_time
196
+ print(f"\n\n{DIM}✦ {format_time(elapsed)}{RESET}\n")
197
+
198
+ except KeyboardInterrupt:
199
+ continue
200
+ except EOFError:
201
+ print(f"{RESET}\n👋")
202
+ break
203
+ except (httpx.ReadError, httpx.ConnectError):
204
+ print(f"{RESET}🔄 Reconnecting...", end="", flush=True)
205
+ time.sleep(1)
206
+ print(CLEAR_LINE, end="")
207
+ if messages:
208
+ messages.pop()
209
+
210
+ def main():
211
+ if len(sys.argv) < 3 or sys.argv[1] != "chat":
212
+ print("Usage: cycls chat <url>")
213
+ sys.exit(1)
214
+ chat(sys.argv[2])
215
+
216
+ if __name__ == "__main__":
217
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cycls
3
- Version: 0.0.2.70
3
+ Version: 0.0.2.71
4
4
  Summary: Distribute Intelligence
5
5
  Author: Mohammed J. AlRujayi
6
6
  Author-email: mj@cycls.com
@@ -1,5 +1,6 @@
1
1
  cycls/__init__.py,sha256=bVT0dYTXLdSC3ZURgtm-DEOj-VO6RUM6zGsJB0zuj6Y,61
2
2
  cycls/auth.py,sha256=xkndHZyCfnlertMMEKerCJjf23N3fVcTRVTTSXTTuzg,247
3
+ cycls/cli.py,sha256=AKf0z7ZLau3GvBVR_IhB7agmq4nVaHkcuUafNyvv2_A,7978
3
4
  cycls/default-theme/assets/index-B0ZKcm_V.css,sha256=wK9-NhEB8xPcN9Zv69zpOcfGTlFbMwyC9WqTmSKUaKw,6546
4
5
  cycls/default-theme/assets/index-D5EDcI4J.js,sha256=sN4qRcAXa7DBd9JzmVcCoCwH4l8cNCM-U9QGUjBvWSo,1346506
5
6
  cycls/default-theme/index.html,sha256=bM-yW_g0cGrV40Q5yY3ccY0fM4zI1Wuu5I8EtGFJIxs,828
@@ -7,6 +8,7 @@ cycls/dev-theme/index.html,sha256=QJBHkdNuMMiwQU7o8dN8__8YQeQB45D37D-NCXIWB2Q,11
7
8
  cycls/runtime.py,sha256=LgOrQ6Arh-GWSJqckfra-CvjeSekTvGvjHOBxu7JTQQ,21408
8
9
  cycls/sdk.py,sha256=6oRKP44TJN9HKdNw9OYzDlZFDUMUhoCMt8TEwyu26dI,7368
9
10
  cycls/web.py,sha256=3M3qaWTNY3dpgd7Vq5aXREp-cIFsHrDqBQ1YkGrOaUk,4659
10
- cycls-0.0.2.70.dist-info/METADATA,sha256=CNYz8lGGGn3lIutuVV4oIy_6e0q4eg8ghkNIErNt5p4,8008
11
- cycls-0.0.2.70.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
12
- cycls-0.0.2.70.dist-info/RECORD,,
11
+ cycls-0.0.2.71.dist-info/METADATA,sha256=MG66f3JLtPXNMLV1rCs29BYkBxk2B3aMIG5_azsD9HI,8008
12
+ cycls-0.0.2.71.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
13
+ cycls-0.0.2.71.dist-info/entry_points.txt,sha256=vEhqUxFhhuzCKWtq02LbMnT3wpUqdfgcM3Yh-jjXom8,40
14
+ cycls-0.0.2.71.dist-info/RECORD,,
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ cycls=cycls.cli:main
3
+