codicent-cli 0.8.0__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.0 → codicent_cli-0.8.2}/PKG-INFO +4 -1
- {codicent_cli-0.8.0 → codicent_cli-0.8.2}/app.py +107 -2
- {codicent_cli-0.8.0 → codicent_cli-0.8.2}/codicent_cli.egg-info/PKG-INFO +4 -1
- {codicent_cli-0.8.0 → codicent_cli-0.8.2}/codicent_cli.egg-info/SOURCES.txt +1 -0
- {codicent_cli-0.8.0 → codicent_cli-0.8.2}/codicent_cli.egg-info/requires.txt +3 -0
- {codicent_cli-0.8.0 → codicent_cli-0.8.2}/codicent_cli.egg-info/top_level.txt +1 -0
- {codicent_cli-0.8.0 → codicent_cli-0.8.2}/setup.py +5 -2
- codicent_cli-0.8.2/terminal.py +247 -0
- codicent_cli-0.8.2/wrap.py +877 -0
- codicent_cli-0.8.0/wrap.py +0 -425
- {codicent_cli-0.8.0 → codicent_cli-0.8.2}/LICENSE +0 -0
- {codicent_cli-0.8.0 → codicent_cli-0.8.2}/README.md +0 -0
- {codicent_cli-0.8.0 → codicent_cli-0.8.2}/auth.py +0 -0
- {codicent_cli-0.8.0 → codicent_cli-0.8.2}/codicent_cli.egg-info/dependency_links.txt +0 -0
- {codicent_cli-0.8.0 → codicent_cli-0.8.2}/codicent_cli.egg-info/entry_points.txt +0 -0
- {codicent_cli-0.8.0 → codicent_cli-0.8.2}/setup.cfg +0 -0
- {codicent_cli-0.8.0 → codicent_cli-0.8.2}/templates/__init__.py +0 -0
- {codicent_cli-0.8.0 → codicent_cli-0.8.2}/templates/default.json +0 -0
- {codicent_cli-0.8.0 → codicent_cli-0.8.2}/templates/document-processing.json +0 -0
- {codicent_cli-0.8.0 → codicent_cli-0.8.2}/templates/koi-studio.json +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
|
|
@@ -28,6 +28,9 @@ Requires-Dist: codicent-py
|
|
|
28
28
|
Requires-Dist: prompt_toolkit
|
|
29
29
|
Requires-Dist: requests
|
|
30
30
|
Requires-Dist: pyyaml
|
|
31
|
+
Requires-Dist: packaging
|
|
32
|
+
Requires-Dist: pyte
|
|
33
|
+
Requires-Dist: websocket-client
|
|
31
34
|
Dynamic: author
|
|
32
35
|
Dynamic: author-email
|
|
33
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,8 +131,15 @@ EXAMPLES:
|
|
|
114
131
|
codicent upload document.txt *.log
|
|
115
132
|
codicent init --template koi-studio
|
|
116
133
|
codicent wrap claude
|
|
134
|
+
codicent terminal
|
|
135
|
+
codicent update
|
|
136
|
+
codicent update --install
|
|
117
137
|
echo "Help me debug this" | codicent
|
|
118
138
|
|
|
139
|
+
UPDATE:
|
|
140
|
+
update Check if a newer version is available on PyPI
|
|
141
|
+
--install Also upgrade immediately via pip
|
|
142
|
+
|
|
119
143
|
ENVIRONMENT:
|
|
120
144
|
CODICENT_TOKEN Your Codicent API token (fallback if no cached auth)
|
|
121
145
|
|
|
@@ -123,9 +147,11 @@ For more information, visit: https://github.com/izaxon/codicent-cli
|
|
|
123
147
|
"""
|
|
124
148
|
print(help_text.strip())
|
|
125
149
|
|
|
150
|
+
CURRENT_VERSION = "0.8.1"
|
|
151
|
+
|
|
126
152
|
def show_version():
|
|
127
153
|
"""Display version information."""
|
|
128
|
-
print("Codicent CLI
|
|
154
|
+
print(f"Codicent CLI v{CURRENT_VERSION}")
|
|
129
155
|
|
|
130
156
|
def validate_input(question):
|
|
131
157
|
"""Validate user input."""
|
|
@@ -200,6 +226,54 @@ def main():
|
|
|
200
226
|
console.print("[dim]Run 'codicent auth' to authenticate[/dim]")
|
|
201
227
|
return 1
|
|
202
228
|
|
|
229
|
+
# Handle update command
|
|
230
|
+
if len(sys.argv) > 1 and sys.argv[1] == "update":
|
|
231
|
+
do_install = "--install" in sys.argv
|
|
232
|
+
try:
|
|
233
|
+
resp = requests.get(
|
|
234
|
+
"https://pypi.org/pypi/codicent-cli/json",
|
|
235
|
+
timeout=10,
|
|
236
|
+
)
|
|
237
|
+
resp.raise_for_status()
|
|
238
|
+
latest = resp.json()["info"]["version"]
|
|
239
|
+
except Exception as e:
|
|
240
|
+
console.print(f"[red]❌ Could not check PyPI: {e}[/red]")
|
|
241
|
+
return 1
|
|
242
|
+
|
|
243
|
+
from packaging.version import Version
|
|
244
|
+
|
|
245
|
+
# Detect whether we're running inside a pipx-managed environment
|
|
246
|
+
is_pipx = "pipx" in sys.executable.replace("\\", "/")
|
|
247
|
+
|
|
248
|
+
if Version(latest) > Version(CURRENT_VERSION):
|
|
249
|
+
console.print(
|
|
250
|
+
f"[yellow]⬆ New version available:[/yellow] "
|
|
251
|
+
f"[bold]{latest}[/bold] (you have {CURRENT_VERSION})"
|
|
252
|
+
)
|
|
253
|
+
if do_install:
|
|
254
|
+
if is_pipx:
|
|
255
|
+
console.print("[dim]Running pipx upgrade codicent-cli...[/dim]")
|
|
256
|
+
result = subprocess.run(
|
|
257
|
+
["pipx", "upgrade", "codicent-cli"],
|
|
258
|
+
capture_output=False,
|
|
259
|
+
)
|
|
260
|
+
else:
|
|
261
|
+
console.print("[dim]Running pip install --upgrade codicent-cli...[/dim]")
|
|
262
|
+
result = subprocess.run(
|
|
263
|
+
[sys.executable, "-m", "pip", "install", "--upgrade", "codicent-cli"],
|
|
264
|
+
capture_output=False,
|
|
265
|
+
)
|
|
266
|
+
return result.returncode
|
|
267
|
+
else:
|
|
268
|
+
upgrade_cmd = "pipx upgrade codicent-cli" if is_pipx else "pip install --upgrade codicent-cli"
|
|
269
|
+
console.print(
|
|
270
|
+
f"[dim]Run [bold]codicent update --install[/bold] to upgrade, "
|
|
271
|
+
f"or [bold]{upgrade_cmd}[/bold][/dim]"
|
|
272
|
+
)
|
|
273
|
+
else:
|
|
274
|
+
console.print(f"[green]✅ Already up to date (v{CURRENT_VERSION})[/green]")
|
|
275
|
+
return 0
|
|
276
|
+
|
|
203
277
|
# Handle upload command
|
|
204
278
|
if len(sys.argv) > 1 and sys.argv[1] == "upload":
|
|
205
279
|
if len(sys.argv) < 3:
|
|
@@ -499,6 +573,37 @@ def main():
|
|
|
499
573
|
project=wrap_project,
|
|
500
574
|
)
|
|
501
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
|
+
|
|
502
607
|
# Get authentication token
|
|
503
608
|
token = auth.get_cached_token()
|
|
504
609
|
if not token:
|
|
@@ -727,7 +832,7 @@ def main():
|
|
|
727
832
|
|
|
728
833
|
header = Text()
|
|
729
834
|
header.append("Codicent CLI", style="bold bright_cyan")
|
|
730
|
-
header.append("
|
|
835
|
+
header.append(f" v{CURRENT_VERSION}\n", style="bright_cyan")
|
|
731
836
|
header.append("Type your questions or use Ctrl+C to exit.\n", style="dim")
|
|
732
837
|
header.append("\n")
|
|
733
838
|
header.append("Tip: ", style="bold dim")
|
|
@@ -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
|
|
@@ -28,6 +28,9 @@ Requires-Dist: codicent-py
|
|
|
28
28
|
Requires-Dist: prompt_toolkit
|
|
29
29
|
Requires-Dist: requests
|
|
30
30
|
Requires-Dist: pyyaml
|
|
31
|
+
Requires-Dist: packaging
|
|
32
|
+
Requires-Dist: pyte
|
|
33
|
+
Requires-Dist: websocket-client
|
|
31
34
|
Dynamic: author
|
|
32
35
|
Dynamic: author-email
|
|
33
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',
|
|
@@ -45,6 +45,9 @@ setup(
|
|
|
45
45
|
'prompt_toolkit',
|
|
46
46
|
'requests',
|
|
47
47
|
'pyyaml',
|
|
48
|
+
'packaging',
|
|
49
|
+
'pyte',
|
|
50
|
+
'websocket-client',
|
|
48
51
|
],
|
|
49
52
|
entry_points={
|
|
50
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()
|