codicent-cli 0.8.1__tar.gz → 0.8.2__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.
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/PKG-INFO +2 -1
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/app.py +49 -0
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/codicent_cli.egg-info/PKG-INFO +2 -1
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/codicent_cli.egg-info/SOURCES.txt +1 -0
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/codicent_cli.egg-info/requires.txt +1 -0
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/codicent_cli.egg-info/top_level.txt +1 -0
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/setup.py +3 -2
- codicent_cli-0.8.2/terminal.py +247 -0
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/LICENSE +0 -0
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/README.md +0 -0
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/auth.py +0 -0
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/codicent_cli.egg-info/dependency_links.txt +0 -0
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/codicent_cli.egg-info/entry_points.txt +0 -0
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/setup.cfg +0 -0
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/templates/__init__.py +0 -0
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/templates/default.json +0 -0
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/templates/document-processing.json +0 -0
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/templates/koi-studio.json +0 -0
- {codicent_cli-0.8.1 → codicent_cli-0.8.2}/wrap.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codicent-cli
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.2
|
|
4
4
|
Summary: Command-line interface for the Codicent API
|
|
5
5
|
Home-page: https://github.com/izaxon/codicent-cli
|
|
6
6
|
Author: Johan Isaksson
|
|
@@ -30,6 +30,7 @@ Requires-Dist: requests
|
|
|
30
30
|
Requires-Dist: pyyaml
|
|
31
31
|
Requires-Dist: packaging
|
|
32
32
|
Requires-Dist: pyte
|
|
33
|
+
Requires-Dist: websocket-client
|
|
33
34
|
Dynamic: author
|
|
34
35
|
Dynamic: author-email
|
|
35
36
|
Dynamic: classifier
|
|
@@ -106,6 +106,23 @@ WRAP (AI CLI Remote Bridge):
|
|
|
106
106
|
codicent wrap --poll-interval 5 codex
|
|
107
107
|
codicent wrap --binary /usr/local/bin/claude claude --model claude-opus-4-5
|
|
108
108
|
|
|
109
|
+
TERMINAL (Web Terminal Host):
|
|
110
|
+
terminal Start a local shell and host it as a Codicent terminal session.
|
|
111
|
+
Browser users can connect to it via the Codicent web UI.
|
|
112
|
+
|
|
113
|
+
[shell] Shell to run (default: $SHELL or bash)
|
|
114
|
+
|
|
115
|
+
How it works:
|
|
116
|
+
1. Spawns the shell locally using a PTY
|
|
117
|
+
2. Connects to the Codicent server as a "terminal host" via WebSocket
|
|
118
|
+
3. Relays shell I/O to any browser that connects to the session
|
|
119
|
+
4. Session output is logged as #terminal-session in Codicent when done
|
|
120
|
+
|
|
121
|
+
Examples:
|
|
122
|
+
codicent terminal
|
|
123
|
+
codicent terminal bash
|
|
124
|
+
codicent terminal zsh
|
|
125
|
+
|
|
109
126
|
EXAMPLES:
|
|
110
127
|
codicent auth
|
|
111
128
|
codicent "What is Python?"
|
|
@@ -114,6 +131,7 @@ EXAMPLES:
|
|
|
114
131
|
codicent upload document.txt *.log
|
|
115
132
|
codicent init --template koi-studio
|
|
116
133
|
codicent wrap claude
|
|
134
|
+
codicent terminal
|
|
117
135
|
codicent update
|
|
118
136
|
codicent update --install
|
|
119
137
|
echo "Help me debug this" | codicent
|
|
@@ -555,6 +573,37 @@ def main():
|
|
|
555
573
|
project=wrap_project,
|
|
556
574
|
)
|
|
557
575
|
|
|
576
|
+
# Handle terminal command
|
|
577
|
+
if len(sys.argv) > 1 and sys.argv[1] == "terminal":
|
|
578
|
+
from terminal import run_terminal_host
|
|
579
|
+
|
|
580
|
+
term_token = auth.get_cached_token()
|
|
581
|
+
if not term_token:
|
|
582
|
+
term_token = os.getenv("CODICENT_TOKEN")
|
|
583
|
+
if not term_token:
|
|
584
|
+
console.print("[red]❌ No authentication found.[/red]")
|
|
585
|
+
console.print("[dim]Run 'codicent auth' to authenticate, or set CODICENT_TOKEN[/dim]")
|
|
586
|
+
return 1
|
|
587
|
+
|
|
588
|
+
try:
|
|
589
|
+
term_payload = term_token.split(".")[1]
|
|
590
|
+
term_payload += '=' * (-len(term_payload) % 4)
|
|
591
|
+
term_jwt = json.loads(base64.urlsafe_b64decode(term_payload).decode('utf-8'))
|
|
592
|
+
term_project = term_jwt["project"]
|
|
593
|
+
term_base_url = term_jwt.get("baseUrl", "https://codicent.com/")
|
|
594
|
+
except Exception as e:
|
|
595
|
+
console.print(f"[red]❌ Failed to extract project from token: {e}[/red]")
|
|
596
|
+
return 1
|
|
597
|
+
|
|
598
|
+
shell_arg = sys.argv[2] if len(sys.argv) > 2 else None
|
|
599
|
+
run_terminal_host(
|
|
600
|
+
token=term_token,
|
|
601
|
+
base_url=term_base_url,
|
|
602
|
+
project=term_project,
|
|
603
|
+
shell=shell_arg,
|
|
604
|
+
)
|
|
605
|
+
return 0
|
|
606
|
+
|
|
558
607
|
# Get authentication token
|
|
559
608
|
token = auth.get_cached_token()
|
|
560
609
|
if not token:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codicent-cli
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.2
|
|
4
4
|
Summary: Command-line interface for the Codicent API
|
|
5
5
|
Home-page: https://github.com/izaxon/codicent-cli
|
|
6
6
|
Author: Johan Isaksson
|
|
@@ -30,6 +30,7 @@ Requires-Dist: requests
|
|
|
30
30
|
Requires-Dist: pyyaml
|
|
31
31
|
Requires-Dist: packaging
|
|
32
32
|
Requires-Dist: pyte
|
|
33
|
+
Requires-Dist: websocket-client
|
|
33
34
|
Dynamic: author
|
|
34
35
|
Dynamic: author-email
|
|
35
36
|
Dynamic: classifier
|
|
@@ -9,7 +9,7 @@ except FileNotFoundError:
|
|
|
9
9
|
|
|
10
10
|
setup(
|
|
11
11
|
name='codicent-cli',
|
|
12
|
-
version='0.8.
|
|
12
|
+
version='0.8.2',
|
|
13
13
|
author='Johan Isaksson',
|
|
14
14
|
author_email='johan@izaxon.com',
|
|
15
15
|
description='Command-line interface for the Codicent API',
|
|
@@ -35,7 +35,7 @@ setup(
|
|
|
35
35
|
'Programming Language :: Python :: 3.12',
|
|
36
36
|
],
|
|
37
37
|
keywords='codicent cli api chat ai',
|
|
38
|
-
py_modules=['app', 'auth', 'wrap'],
|
|
38
|
+
py_modules=['app', 'auth', 'wrap', 'terminal'],
|
|
39
39
|
packages=['templates'],
|
|
40
40
|
package_data={'templates': ['*.json']},
|
|
41
41
|
python_requires='>=3.6',
|
|
@@ -47,6 +47,7 @@ setup(
|
|
|
47
47
|
'pyyaml',
|
|
48
48
|
'packaging',
|
|
49
49
|
'pyte',
|
|
50
|
+
'websocket-client',
|
|
50
51
|
],
|
|
51
52
|
entry_points={
|
|
52
53
|
'console_scripts': [
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
codicent terminal — web terminal host
|
|
3
|
+
|
|
4
|
+
Spawns a local shell process and registers it as a terminal session on the
|
|
5
|
+
Codicent server. Browser users can then connect to it via the web UI.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
codicent terminal [shell] # shell defaults to $SHELL or bash
|
|
9
|
+
|
|
10
|
+
The CLI tool:
|
|
11
|
+
1. Spawns the shell locally via PTY (or subprocess on Windows)
|
|
12
|
+
2. Connects to /terminal/host on the Codicent server via WebSocket
|
|
13
|
+
3. Relays PTY I/O <-> WebSocket in real time
|
|
14
|
+
4. Session output is logged to Codicent as #terminal-session when it ends
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
import threading
|
|
20
|
+
import signal
|
|
21
|
+
import uuid
|
|
22
|
+
import logging
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import websocket as websocket_client # websocket-client library
|
|
26
|
+
_HAS_WS = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
_HAS_WS = False
|
|
29
|
+
|
|
30
|
+
# PTY support (Linux/macOS/WSL); falls back to subprocess on Windows
|
|
31
|
+
try:
|
|
32
|
+
import pty
|
|
33
|
+
import termios
|
|
34
|
+
import fcntl
|
|
35
|
+
import struct
|
|
36
|
+
_HAS_PTY = True
|
|
37
|
+
except ImportError:
|
|
38
|
+
_HAS_PTY = False
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _get_terminal_size():
|
|
44
|
+
try:
|
|
45
|
+
import shutil
|
|
46
|
+
sz = shutil.get_terminal_size()
|
|
47
|
+
return sz.columns, sz.lines
|
|
48
|
+
except Exception:
|
|
49
|
+
return 80, 24
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run_terminal_host(token: str, base_url: str, project: str, shell: str = None):
|
|
53
|
+
"""Start a local shell and host it as a Codicent terminal session."""
|
|
54
|
+
|
|
55
|
+
if not _HAS_WS:
|
|
56
|
+
print("[error] websocket-client is required: pip install websocket-client", file=sys.stderr)
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
shell = shell or os.environ.get("SHELL", "bash")
|
|
60
|
+
session_id = uuid.uuid4().hex
|
|
61
|
+
|
|
62
|
+
# Build WebSocket URL
|
|
63
|
+
ws_base = base_url.rstrip("/").replace("https://", "wss://").replace("http://", "ws://")
|
|
64
|
+
ws_url = (
|
|
65
|
+
f"{ws_base}/terminal/host"
|
|
66
|
+
f"?project={project}"
|
|
67
|
+
f"&command={os.path.basename(shell)}"
|
|
68
|
+
f"&sessionId={session_id}"
|
|
69
|
+
f"&access_token={token}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
print(f"[codicent terminal] project=@{project} shell={shell} session={session_id}")
|
|
73
|
+
print(f"[codicent terminal] Connecting to {base_url} ...")
|
|
74
|
+
|
|
75
|
+
stop_event = threading.Event()
|
|
76
|
+
ws_app = None
|
|
77
|
+
|
|
78
|
+
# ── PTY-based (POSIX) ─────────────────────────────────────────────
|
|
79
|
+
if _HAS_PTY:
|
|
80
|
+
cols, rows = _get_terminal_size()
|
|
81
|
+
|
|
82
|
+
# Fork a PTY
|
|
83
|
+
pid, fd = pty.fork()
|
|
84
|
+
|
|
85
|
+
if pid == 0:
|
|
86
|
+
# Child: exec the shell
|
|
87
|
+
env = os.environ.copy()
|
|
88
|
+
env["TERM"] = "xterm-256color"
|
|
89
|
+
os.execvpe(shell, [shell, "-i"], env)
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
|
|
92
|
+
# Parent: relay PTY <-> WebSocket
|
|
93
|
+
|
|
94
|
+
def _set_pty_size(c, r):
|
|
95
|
+
try:
|
|
96
|
+
import struct, termios, fcntl
|
|
97
|
+
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", r, c, 0, 0))
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
_set_pty_size(cols, rows)
|
|
102
|
+
|
|
103
|
+
def on_open(ws):
|
|
104
|
+
print("[codicent terminal] Connected. Shell is ready in the browser.")
|
|
105
|
+
# Send initial resize
|
|
106
|
+
import json
|
|
107
|
+
ws.send(json.dumps({"type": "resize", "cols": cols, "rows": rows}))
|
|
108
|
+
|
|
109
|
+
def on_message(ws, message):
|
|
110
|
+
# Incoming from browser: either resize JSON or raw stdin
|
|
111
|
+
try:
|
|
112
|
+
import json
|
|
113
|
+
obj = json.loads(message)
|
|
114
|
+
if obj.get("type") == "resize":
|
|
115
|
+
_set_pty_size(obj.get("cols", 80), obj.get("rows", 24))
|
|
116
|
+
return
|
|
117
|
+
except (ValueError, KeyError):
|
|
118
|
+
pass
|
|
119
|
+
# Write to PTY stdin
|
|
120
|
+
try:
|
|
121
|
+
os.write(fd, message.encode() if isinstance(message, str) else message)
|
|
122
|
+
except OSError:
|
|
123
|
+
ws.close()
|
|
124
|
+
|
|
125
|
+
def on_error(ws, error):
|
|
126
|
+
print(f"[codicent terminal] WebSocket error: {error}", file=sys.stderr)
|
|
127
|
+
|
|
128
|
+
def on_close(ws, code, reason):
|
|
129
|
+
print(f"[codicent terminal] Disconnected ({code}). Session ended.")
|
|
130
|
+
stop_event.set()
|
|
131
|
+
|
|
132
|
+
ws_app = websocket_client.WebSocketApp(
|
|
133
|
+
ws_url,
|
|
134
|
+
on_open=on_open,
|
|
135
|
+
on_message=on_message,
|
|
136
|
+
on_error=on_error,
|
|
137
|
+
on_close=on_close,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Thread: read PTY output and forward to WebSocket
|
|
141
|
+
def _pty_reader():
|
|
142
|
+
import select as sel
|
|
143
|
+
while not stop_event.is_set():
|
|
144
|
+
try:
|
|
145
|
+
r, _, _ = sel.select([fd], [], [], 0.05)
|
|
146
|
+
if not r:
|
|
147
|
+
continue
|
|
148
|
+
data = os.read(fd, 4096)
|
|
149
|
+
if not data:
|
|
150
|
+
break
|
|
151
|
+
if ws_app.sock and ws_app.sock.connected:
|
|
152
|
+
ws_app.send(data.decode("utf-8", errors="replace"))
|
|
153
|
+
except OSError:
|
|
154
|
+
break
|
|
155
|
+
except Exception as e:
|
|
156
|
+
logger.debug(f"PTY reader: {e}")
|
|
157
|
+
stop_event.set()
|
|
158
|
+
if ws_app.sock:
|
|
159
|
+
ws_app.close()
|
|
160
|
+
|
|
161
|
+
t = threading.Thread(target=_pty_reader, daemon=True)
|
|
162
|
+
t.start()
|
|
163
|
+
|
|
164
|
+
def _sigchld_handler(signum, frame):
|
|
165
|
+
stop_event.set()
|
|
166
|
+
|
|
167
|
+
signal.signal(signal.SIGCHLD, _sigchld_handler)
|
|
168
|
+
|
|
169
|
+
ws_app.run_forever()
|
|
170
|
+
|
|
171
|
+
# Clean up child process
|
|
172
|
+
try:
|
|
173
|
+
os.kill(pid, signal.SIGTERM)
|
|
174
|
+
except ProcessLookupError:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
# ── Subprocess-based (Windows fallback) ──────────────────────────
|
|
178
|
+
else:
|
|
179
|
+
import subprocess
|
|
180
|
+
import queue
|
|
181
|
+
|
|
182
|
+
proc = subprocess.Popen(
|
|
183
|
+
[shell],
|
|
184
|
+
stdin=subprocess.PIPE,
|
|
185
|
+
stdout=subprocess.PIPE,
|
|
186
|
+
stderr=subprocess.STDOUT,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
output_queue: queue.Queue = queue.Queue()
|
|
190
|
+
|
|
191
|
+
def _stdout_reader():
|
|
192
|
+
while True:
|
|
193
|
+
chunk = proc.stdout.read(4096)
|
|
194
|
+
if not chunk:
|
|
195
|
+
break
|
|
196
|
+
output_queue.put(chunk.decode("utf-8", errors="replace"))
|
|
197
|
+
stop_event.set()
|
|
198
|
+
|
|
199
|
+
threading.Thread(target=_stdout_reader, daemon=True).start()
|
|
200
|
+
|
|
201
|
+
def on_open(ws):
|
|
202
|
+
print("[codicent terminal] Connected.")
|
|
203
|
+
|
|
204
|
+
# Thread: drain queue -> WebSocket
|
|
205
|
+
def _sender():
|
|
206
|
+
while not stop_event.is_set():
|
|
207
|
+
try:
|
|
208
|
+
text = output_queue.get(timeout=0.05)
|
|
209
|
+
if ws.sock and ws.sock.connected:
|
|
210
|
+
ws.send(text)
|
|
211
|
+
except queue.Empty:
|
|
212
|
+
continue
|
|
213
|
+
except Exception:
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
threading.Thread(target=_sender, daemon=True).start()
|
|
217
|
+
|
|
218
|
+
def on_message(ws, message):
|
|
219
|
+
try:
|
|
220
|
+
import json
|
|
221
|
+
obj = json.loads(message)
|
|
222
|
+
if obj.get("type") == "resize":
|
|
223
|
+
return # resize not applicable without PTY
|
|
224
|
+
except (ValueError, KeyError):
|
|
225
|
+
pass
|
|
226
|
+
try:
|
|
227
|
+
proc.stdin.write((message if isinstance(message, bytes) else message.encode()))
|
|
228
|
+
proc.stdin.flush()
|
|
229
|
+
except OSError:
|
|
230
|
+
ws.close()
|
|
231
|
+
|
|
232
|
+
def on_error(ws, error):
|
|
233
|
+
print(f"[codicent terminal] WebSocket error: {error}", file=sys.stderr)
|
|
234
|
+
|
|
235
|
+
def on_close(ws, code, reason):
|
|
236
|
+
print("[codicent terminal] Disconnected.")
|
|
237
|
+
stop_event.set()
|
|
238
|
+
proc.terminate()
|
|
239
|
+
|
|
240
|
+
ws_app = websocket_client.WebSocketApp(
|
|
241
|
+
ws_url,
|
|
242
|
+
on_open=on_open,
|
|
243
|
+
on_message=on_message,
|
|
244
|
+
on_error=on_error,
|
|
245
|
+
on_close=on_close,
|
|
246
|
+
)
|
|
247
|
+
ws_app.run_forever()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|