cycls 0.0.2.79__tar.gz → 0.0.2.80__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cycls
3
- Version: 0.0.2.79
3
+ Version: 0.0.2.80
4
4
  Summary: Distribute Intelligence
5
5
  Author: Mohammed J. AlRujayi
6
6
  Author-email: mj@cycls.com
@@ -47,6 +47,8 @@ Distribute Intelligence
47
47
 
48
48
  The open-source SDK for distributing AI agents.
49
49
 
50
+ The function is the unit of abstraction in cycls. Your agent logic lives in a plain Python function — the decorator layers on everything else: containerization, authentication, deployment, analytics. You write the function, the `@` handles the infrastructure.
51
+
50
52
  ## Distribute Intelligence
51
53
 
52
54
  Write a function. Deploy it as an API, a web interface, or both. Add authentication, analytics, and monetization with flags.
@@ -22,6 +22,8 @@ Distribute Intelligence
22
22
 
23
23
  The open-source SDK for distributing AI agents.
24
24
 
25
+ The function is the unit of abstraction in cycls. Your agent logic lives in a plain Python function — the decorator layers on everything else: containerization, authentication, deployment, analytics. You write the function, the `@` handles the infrastructure.
26
+
25
27
  ## Distribute Intelligence
26
28
 
27
29
  Write a function. Deploy it as an API, a web interface, or both. Add authentication, analytics, and monetization with flags.
@@ -0,0 +1,95 @@
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
+ len(sys.argv) < 2 and (print("Usage: cycls chat <url|port>"), sys.exit(1))
89
+ arg = sys.argv[1]
90
+ url = f"http://localhost:{arg}" if arg.isdigit() else arg
91
+ chat(url)
92
+
93
+
94
+ if __name__ == "__main__":
95
+ main()
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "cycls"
3
- version = "0.0.2.79"
3
+ version = "0.0.2.80"
4
4
 
5
5
  packages = [{ include = "cycls" }]
6
6
  include = ["cycls/theme/**/*"]
@@ -19,7 +19,7 @@ grpcio = "^1.76.0"
19
19
  protobuf = "^6.0"
20
20
 
21
21
  [tool.poetry.scripts]
22
- cycls = "cycls.cli:main"
22
+ cycls = "cycls.chat:main"
23
23
 
24
24
  [tool.poetry.group.test.dependencies]
25
25
  pytest = "^8.0.0"
@@ -1,217 +0,0 @@
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()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes