zephyr-zecode 0.4.5__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.
- zephyr_zecode-0.4.5/PKG-INFO +113 -0
- zephyr_zecode-0.4.5/PYPI_README.md +90 -0
- zephyr_zecode-0.4.5/pyproject.toml +42 -0
- zephyr_zecode-0.4.5/setup.cfg +4 -0
- zephyr_zecode-0.4.5/zecode_terminal/__init__.py +4 -0
- zephyr_zecode-0.4.5/zecode_terminal/__main__.py +287 -0
- zephyr_zecode-0.4.5/zecode_terminal/app.py +2032 -0
- zephyr_zecode-0.4.5/zecode_terminal/client.py +378 -0
- zephyr_zecode-0.4.5/zecode_terminal/inline_diff.py +125 -0
- zephyr_zecode-0.4.5/zecode_terminal/native.py +767 -0
- zephyr_zecode-0.4.5/zecode_terminal/native_history.py +46 -0
- zephyr_zecode-0.4.5/zecode_terminal/native_menus.py +393 -0
- zephyr_zecode-0.4.5/zecode_terminal/native_render.py +129 -0
- zephyr_zecode-0.4.5/zecode_terminal/review.py +205 -0
- zephyr_zecode-0.4.5/zecode_terminal/secrets.py +338 -0
- zephyr_zecode-0.4.5/zecode_terminal/terminal_io.py +524 -0
- zephyr_zecode-0.4.5/zecode_terminal/tui.py +5499 -0
- zephyr_zecode-0.4.5/zecode_terminal/updater.py +60 -0
- zephyr_zecode-0.4.5/zephyr_zecode.egg-info/PKG-INFO +113 -0
- zephyr_zecode-0.4.5/zephyr_zecode.egg-info/SOURCES.txt +22 -0
- zephyr_zecode-0.4.5/zephyr_zecode.egg-info/dependency_links.txt +1 -0
- zephyr_zecode-0.4.5/zephyr_zecode.egg-info/entry_points.txt +2 -0
- zephyr_zecode-0.4.5/zephyr_zecode.egg-info/requires.txt +8 -0
- zephyr_zecode-0.4.5/zephyr_zecode.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zephyr-zecode
|
|
3
|
+
Version: 0.4.5
|
|
4
|
+
Summary: PowerShell-native ZeCode terminal client for Zephyr
|
|
5
|
+
Author: Zephyr Project
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Keywords: zecode,zephyr,coding-agent,terminal
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Topic :: Software Development
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: mcp<2,>=1.27
|
|
17
|
+
Requires-Dist: httpx<1,>=0.27
|
|
18
|
+
Requires-Dist: arabic-reshaper<4,>=3
|
|
19
|
+
Requires-Dist: python-bidi<1,>=0.6
|
|
20
|
+
Requires-Dist: rich<15,>=13.7
|
|
21
|
+
Provides-Extra: classic
|
|
22
|
+
Requires-Dist: textual<7,>=0.85; extra == "classic"
|
|
23
|
+
|
|
24
|
+
# ZeCode 0.4.5
|
|
25
|
+
|
|
26
|
+
ZeCode is the PowerShell-native terminal client for Zephyr. It provides one
|
|
27
|
+
chronological conversation with inline tool activity, verified file delivery,
|
|
28
|
+
on-demand terminal menus, and secure Zephyr Gateway v2 device pairing.
|
|
29
|
+
|
|
30
|
+
Version `0.4.5` uses native terminal input and Rich output, without importing
|
|
31
|
+
Textual or using CSS in the default launch path. It retains normal terminal
|
|
32
|
+
scrollback and requires Zephyr `1.40.8` for session-free home startup.
|
|
33
|
+
|
|
34
|
+
0.4.5 fixes live group-event filtering, pending-session recovery and the PgUp/Esc screen wipe. Home does not create or attach to a session. Choose Resume explicitly or save automatic resume.
|
|
35
|
+
|
|
36
|
+
## Terminal experience
|
|
37
|
+
|
|
38
|
+
- Top-left native terminal home with model, workspace, mode, and effort.
|
|
39
|
+
- One chronological transcript for assistant text and real tool activity.
|
|
40
|
+
- Stable `File`, `Shell`, `Tests`, `Web Search`, `MCP`, and `Delivery` rows.
|
|
41
|
+
- Left-aligned multiline input with temporary menus; full-screen history and source viewers preserve the primary screen.
|
|
42
|
+
- Slash command search, model/theme/mode pickers, and F2 settings.
|
|
43
|
+
- Failed runs release their client lock immediately and do not duplicate chat.
|
|
44
|
+
|
|
45
|
+
0.4.3 adds batched Windows input, guarded unmarked paste, direct clipboard paste
|
|
46
|
+
with F4 on Windows, and collapsible long drafts (500 characters or six lines).
|
|
47
|
+
F6 toggles full text. Explicit paste waits for Enter. Unmarked input bursts use
|
|
48
|
+
F8 to submit; their Enter characters remain newlines. No backend or model changes.
|
|
49
|
+
|
|
50
|
+
0.4.2 added an explicit startup choice (new/resume/select session), optional
|
|
51
|
+
automatic resume, incremental row drawing with a single flush per update,
|
|
52
|
+
and role-labelled, wrapped conversation history. PgUp reads the current
|
|
53
|
+
conversation while preserving the draft; arriving output waits until Esc/End.
|
|
54
|
+
PageDown stops at the final full page, including after window resizing.
|
|
55
|
+
`/tools` opens coding tools first; media and other integrations live in
|
|
56
|
+
More tools. Browsing these categories does not alter runtime permissions.
|
|
57
|
+
|
|
58
|
+
## Install on Windows
|
|
59
|
+
|
|
60
|
+
Install Python 3.11 or newer, extract the complete release ZIP, then run in
|
|
61
|
+
PowerShell from that folder:
|
|
62
|
+
|
|
63
|
+
```powershell
|
|
64
|
+
Set-ExecutionPolicy -Scope Process Bypass
|
|
65
|
+
.\UPDATE_ZECODE_WINDOWS.ps1
|
|
66
|
+
zecode --version
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Start ZeCode:
|
|
70
|
+
|
|
71
|
+
```powershell
|
|
72
|
+
zecode login
|
|
73
|
+
zecode doctor
|
|
74
|
+
zecode
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The script verifies and installs the bundled wheel via pipx, in the same
|
|
78
|
+
PowerShell window. It prioritizes the pipx executable in the current and saved
|
|
79
|
+
user PATH. Internet is needed for pipx and any missing dependencies.
|
|
80
|
+
|
|
81
|
+
This ZIP is a local owner release, not confirmation of a PyPI publication.
|
|
82
|
+
The following command continues to update from the configured package index;
|
|
83
|
+
use the bundled script to install this exact release:
|
|
84
|
+
|
|
85
|
+
```powershell
|
|
86
|
+
zecode --update
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The installed distribution is named `zephyr-zecode`; the terminal command is
|
|
90
|
+
always `zecode`.
|
|
91
|
+
|
|
92
|
+
## Native controls
|
|
93
|
+
|
|
94
|
+
Enter sends ordinary drafts; F8 sends guarded unmarked pastes. F4 on Windows
|
|
95
|
+
reads the clipboard as one paste, without relying on the host's Ctrl+V binding.
|
|
96
|
+
F6 expands/collapses long pasted drafts. Arrow and deletion keys reveal the
|
|
97
|
+
text before editing. Short pastes remain inline. A compact summary is only a
|
|
98
|
+
display representation; the full draft is sent. Shift+Enter (Windows) or
|
|
99
|
+
Alt+Enter inserts a newline. Ctrl+V on
|
|
100
|
+
Windows and bracketed paste insert multiline text as a single draft. `/` opens
|
|
101
|
+
suggestions, F2 settings, Ctrl+P commands, and Ctrl+L sessions. Tab cycles mode;
|
|
102
|
+
Esc stops an active run. Ctrl+Q exits. `/keyboard` lists all native shortcuts.
|
|
103
|
+
|
|
104
|
+
`/files` searches paths and previews/downloads files; `/diff` previews patches
|
|
105
|
+
and confirms accept/reject of all pending changes. `/mcp`, `/permissions`,
|
|
106
|
+
`/agents` and `/tools` use existing server controls. `/allow`, `/always` and
|
|
107
|
+
`/deny` resolve pending approvals.
|
|
108
|
+
|
|
109
|
+
Syntax themes color previews; terminal font and background remain terminal
|
|
110
|
+
settings. Native shortcuts are fixed in this release; previous Textual key
|
|
111
|
+
remappings do not apply. Historical Textual source remains for compatibility
|
|
112
|
+
tests with the optional `classic` dependency, and is not the default UI.
|
|
113
|
+
Arabic rendering can be selected in Display preferences.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# ZeCode 0.4.5
|
|
2
|
+
|
|
3
|
+
ZeCode is the PowerShell-native terminal client for Zephyr. It provides one
|
|
4
|
+
chronological conversation with inline tool activity, verified file delivery,
|
|
5
|
+
on-demand terminal menus, and secure Zephyr Gateway v2 device pairing.
|
|
6
|
+
|
|
7
|
+
Version `0.4.5` uses native terminal input and Rich output, without importing
|
|
8
|
+
Textual or using CSS in the default launch path. It retains normal terminal
|
|
9
|
+
scrollback and requires Zephyr `1.40.8` for session-free home startup.
|
|
10
|
+
|
|
11
|
+
0.4.5 fixes live group-event filtering, pending-session recovery and the PgUp/Esc screen wipe. Home does not create or attach to a session. Choose Resume explicitly or save automatic resume.
|
|
12
|
+
|
|
13
|
+
## Terminal experience
|
|
14
|
+
|
|
15
|
+
- Top-left native terminal home with model, workspace, mode, and effort.
|
|
16
|
+
- One chronological transcript for assistant text and real tool activity.
|
|
17
|
+
- Stable `File`, `Shell`, `Tests`, `Web Search`, `MCP`, and `Delivery` rows.
|
|
18
|
+
- Left-aligned multiline input with temporary menus; full-screen history and source viewers preserve the primary screen.
|
|
19
|
+
- Slash command search, model/theme/mode pickers, and F2 settings.
|
|
20
|
+
- Failed runs release their client lock immediately and do not duplicate chat.
|
|
21
|
+
|
|
22
|
+
0.4.3 adds batched Windows input, guarded unmarked paste, direct clipboard paste
|
|
23
|
+
with F4 on Windows, and collapsible long drafts (500 characters or six lines).
|
|
24
|
+
F6 toggles full text. Explicit paste waits for Enter. Unmarked input bursts use
|
|
25
|
+
F8 to submit; their Enter characters remain newlines. No backend or model changes.
|
|
26
|
+
|
|
27
|
+
0.4.2 added an explicit startup choice (new/resume/select session), optional
|
|
28
|
+
automatic resume, incremental row drawing with a single flush per update,
|
|
29
|
+
and role-labelled, wrapped conversation history. PgUp reads the current
|
|
30
|
+
conversation while preserving the draft; arriving output waits until Esc/End.
|
|
31
|
+
PageDown stops at the final full page, including after window resizing.
|
|
32
|
+
`/tools` opens coding tools first; media and other integrations live in
|
|
33
|
+
More tools. Browsing these categories does not alter runtime permissions.
|
|
34
|
+
|
|
35
|
+
## Install on Windows
|
|
36
|
+
|
|
37
|
+
Install Python 3.11 or newer, extract the complete release ZIP, then run in
|
|
38
|
+
PowerShell from that folder:
|
|
39
|
+
|
|
40
|
+
```powershell
|
|
41
|
+
Set-ExecutionPolicy -Scope Process Bypass
|
|
42
|
+
.\UPDATE_ZECODE_WINDOWS.ps1
|
|
43
|
+
zecode --version
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Start ZeCode:
|
|
47
|
+
|
|
48
|
+
```powershell
|
|
49
|
+
zecode login
|
|
50
|
+
zecode doctor
|
|
51
|
+
zecode
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The script verifies and installs the bundled wheel via pipx, in the same
|
|
55
|
+
PowerShell window. It prioritizes the pipx executable in the current and saved
|
|
56
|
+
user PATH. Internet is needed for pipx and any missing dependencies.
|
|
57
|
+
|
|
58
|
+
This ZIP is a local owner release, not confirmation of a PyPI publication.
|
|
59
|
+
The following command continues to update from the configured package index;
|
|
60
|
+
use the bundled script to install this exact release:
|
|
61
|
+
|
|
62
|
+
```powershell
|
|
63
|
+
zecode --update
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The installed distribution is named `zephyr-zecode`; the terminal command is
|
|
67
|
+
always `zecode`.
|
|
68
|
+
|
|
69
|
+
## Native controls
|
|
70
|
+
|
|
71
|
+
Enter sends ordinary drafts; F8 sends guarded unmarked pastes. F4 on Windows
|
|
72
|
+
reads the clipboard as one paste, without relying on the host's Ctrl+V binding.
|
|
73
|
+
F6 expands/collapses long pasted drafts. Arrow and deletion keys reveal the
|
|
74
|
+
text before editing. Short pastes remain inline. A compact summary is only a
|
|
75
|
+
display representation; the full draft is sent. Shift+Enter (Windows) or
|
|
76
|
+
Alt+Enter inserts a newline. Ctrl+V on
|
|
77
|
+
Windows and bracketed paste insert multiline text as a single draft. `/` opens
|
|
78
|
+
suggestions, F2 settings, Ctrl+P commands, and Ctrl+L sessions. Tab cycles mode;
|
|
79
|
+
Esc stops an active run. Ctrl+Q exits. `/keyboard` lists all native shortcuts.
|
|
80
|
+
|
|
81
|
+
`/files` searches paths and previews/downloads files; `/diff` previews patches
|
|
82
|
+
and confirms accept/reject of all pending changes. `/mcp`, `/permissions`,
|
|
83
|
+
`/agents` and `/tools` use existing server controls. `/allow`, `/always` and
|
|
84
|
+
`/deny` resolve pending approvals.
|
|
85
|
+
|
|
86
|
+
Syntax themes color previews; terminal font and background remain terminal
|
|
87
|
+
settings. Native shortcuts are fixed in this release; previous Textual key
|
|
88
|
+
remappings do not apply. Historical Textual source remains for compatibility
|
|
89
|
+
tests with the optional `classic` dependency, and is not the default UI.
|
|
90
|
+
Arabic rendering can be selected in Display preferences.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zephyr-zecode"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "PowerShell-native ZeCode terminal client for Zephyr"
|
|
9
|
+
readme = "PYPI_README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "LicenseRef-Proprietary"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Zephyr Project"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["zecode", "zephyr", "coding-agent", "terminal"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Operating System :: Microsoft :: Windows",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Topic :: Software Development",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"mcp>=1.27,<2",
|
|
26
|
+
"httpx>=0.27,<1",
|
|
27
|
+
"arabic-reshaper>=3,<4",
|
|
28
|
+
"python-bidi>=0.6,<1",
|
|
29
|
+
"rich>=13.7,<15",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
classic = ["textual>=0.85,<7"]
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
zecode = "zecode_terminal.__main__:main"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.dynamic]
|
|
39
|
+
version = {attr = "zecode_terminal.__version__"}
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
include = ["zecode_terminal*"]
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
from getpass import getpass
|
|
6
|
+
from importlib import metadata
|
|
7
|
+
import shutil
|
|
8
|
+
import socket
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
from .app import ZeCodeApp
|
|
14
|
+
from . import __version__
|
|
15
|
+
from .client import (
|
|
16
|
+
GatewayBridge,
|
|
17
|
+
MCPBridge,
|
|
18
|
+
begin_device_pairing,
|
|
19
|
+
gateway_health,
|
|
20
|
+
is_transient_disconnect,
|
|
21
|
+
poll_device_pairing,
|
|
22
|
+
)
|
|
23
|
+
from .secrets import (
|
|
24
|
+
clear_gateway_credentials,
|
|
25
|
+
clear_private_link,
|
|
26
|
+
load_gateway_credentials,
|
|
27
|
+
load_gateway_url,
|
|
28
|
+
load_private_link,
|
|
29
|
+
normalize_gateway_url,
|
|
30
|
+
save_gateway_credentials,
|
|
31
|
+
save_private_link,
|
|
32
|
+
)
|
|
33
|
+
from .updater import PYPI_DISTRIBUTION
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _parser() -> argparse.ArgumentParser:
|
|
37
|
+
parser = argparse.ArgumentParser(prog="zecode")
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--version", action="version", version=f"ZeCode {__version__}")
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"command",
|
|
42
|
+
nargs="?",
|
|
43
|
+
choices=("login", "logout", "doctor", "update", "legacy-login"),
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument("--workspace", default="", help="Zephyr workspace key")
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--gateway", default="", help="HTTPS ZeCode Gateway v2 URL")
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--device-name", default="", help="Name shown during device approval")
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--update",
|
|
52
|
+
action="store_true",
|
|
53
|
+
dest="update_requested",
|
|
54
|
+
help="Update ZeCode through pipx in this terminal and exit",
|
|
55
|
+
)
|
|
56
|
+
return parser
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def _run_app(bridge, workspace: str) -> int:
|
|
60
|
+
async with bridge:
|
|
61
|
+
app = ZeCodeApp(bridge, workspace=workspace)
|
|
62
|
+
if not await app.bootstrap():
|
|
63
|
+
return 2
|
|
64
|
+
await app.run()
|
|
65
|
+
bridge.normal_close = app.ui.requested_exit
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def _start(workspace: str) -> int:
|
|
70
|
+
"""Prefer Transport v2; retain the old bridge only for migration."""
|
|
71
|
+
try:
|
|
72
|
+
credentials = load_gateway_credentials()
|
|
73
|
+
except RuntimeError:
|
|
74
|
+
private_link = load_private_link()
|
|
75
|
+
for attempt in range(1, 4):
|
|
76
|
+
bridge = MCPBridge(private_link)
|
|
77
|
+
try:
|
|
78
|
+
return await _run_app(bridge, workspace)
|
|
79
|
+
except BaseException as exc:
|
|
80
|
+
if attempt >= 3 or not is_transient_disconnect(exc):
|
|
81
|
+
raise
|
|
82
|
+
print(
|
|
83
|
+
f"Legacy connection interrupted. Reconnecting ({attempt}/3)…",
|
|
84
|
+
file=sys.stderr,
|
|
85
|
+
)
|
|
86
|
+
await asyncio.sleep(float(attempt))
|
|
87
|
+
return 1
|
|
88
|
+
|
|
89
|
+
for attempt in range(1, 4):
|
|
90
|
+
bridge = GatewayBridge(credentials)
|
|
91
|
+
try:
|
|
92
|
+
return await _run_app(bridge, workspace)
|
|
93
|
+
except BaseException as exc:
|
|
94
|
+
credentials = dict(bridge.credentials)
|
|
95
|
+
if attempt >= 3 or not is_transient_disconnect(exc):
|
|
96
|
+
raise
|
|
97
|
+
print(
|
|
98
|
+
f"ZeCode connection interrupted. Reconnecting ({attempt}/3)…",
|
|
99
|
+
file=sys.stderr,
|
|
100
|
+
)
|
|
101
|
+
await asyncio.sleep(float(attempt))
|
|
102
|
+
return 1
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _gateway_for_login(explicit: str) -> str:
|
|
106
|
+
if str(explicit or "").strip():
|
|
107
|
+
return normalize_gateway_url(explicit)
|
|
108
|
+
stored = load_gateway_url()
|
|
109
|
+
if stored:
|
|
110
|
+
return stored
|
|
111
|
+
entered = input(
|
|
112
|
+
"ZeCode Gateway URL (for example https://host.example/zecode/v2): "
|
|
113
|
+
).strip()
|
|
114
|
+
return normalize_gateway_url(entered)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
async def _login(gateway: str, device_name: str) -> int:
|
|
118
|
+
endpoint = _gateway_for_login(gateway)
|
|
119
|
+
health = await gateway_health(endpoint)
|
|
120
|
+
if not health.get("ok"):
|
|
121
|
+
raise RuntimeError(
|
|
122
|
+
str(health.get("error") or "ZeCode Gateway is not ready"))
|
|
123
|
+
pairing = await begin_device_pairing(
|
|
124
|
+
endpoint,
|
|
125
|
+
device_name=(
|
|
126
|
+
str(device_name or "").strip()
|
|
127
|
+
or "ZeCode on " + socket.gethostname()
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
code = str(pairing.get("user_code") or "")
|
|
131
|
+
command = str(pairing.get("verification_command") or "/zecode-pair " + code)
|
|
132
|
+
print("")
|
|
133
|
+
print("ZeCode device code: " + code)
|
|
134
|
+
print("In your Zephyr Discord server, run:")
|
|
135
|
+
print(" " + command)
|
|
136
|
+
print("Waiting for owner approval…")
|
|
137
|
+
interval = max(1, int(pairing.get("interval") or 2))
|
|
138
|
+
deadline = time.monotonic() + max(30, int(pairing.get("expires_in") or 600))
|
|
139
|
+
while time.monotonic() < deadline:
|
|
140
|
+
result = await poll_device_pairing(
|
|
141
|
+
endpoint, device_code=str(pairing.get("device_code") or ""))
|
|
142
|
+
if result.get("ok"):
|
|
143
|
+
save_gateway_credentials(result)
|
|
144
|
+
clear_private_link()
|
|
145
|
+
print("ZeCode is linked securely to this device.")
|
|
146
|
+
return 0
|
|
147
|
+
await asyncio.sleep(interval)
|
|
148
|
+
raise RuntimeError("The ZeCode device code expired. Run: zecode login")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def _doctor(explicit_gateway: str) -> int:
|
|
152
|
+
endpoint = _gateway_for_login(explicit_gateway)
|
|
153
|
+
print("ZeCode doctor")
|
|
154
|
+
print(" gateway: " + endpoint)
|
|
155
|
+
health = await gateway_health(endpoint)
|
|
156
|
+
print(" HTTPS: " + ("PASS" if health.get("http_status") else "FAIL"))
|
|
157
|
+
print(" Gateway v2: " + ("PASS" if health.get("ok") else "FAIL"))
|
|
158
|
+
print(" Server version: " + str(health.get("version") or "unknown"))
|
|
159
|
+
try:
|
|
160
|
+
credentials = load_gateway_credentials()
|
|
161
|
+
except RuntimeError:
|
|
162
|
+
print(" Device token: NOT LINKED (run: zecode login)")
|
|
163
|
+
return 2
|
|
164
|
+
bridge = GatewayBridge(credentials)
|
|
165
|
+
async with bridge:
|
|
166
|
+
identity = await bridge.probe_identity()
|
|
167
|
+
device = identity.get("device") or {}
|
|
168
|
+
print(" Device token: PASS")
|
|
169
|
+
print(" Device: " + str(device.get("device_name") or "unknown"))
|
|
170
|
+
print(" Transport: HTTPS JSON v2")
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
async def _logout() -> int:
|
|
175
|
+
try:
|
|
176
|
+
credentials = load_gateway_credentials()
|
|
177
|
+
except RuntimeError:
|
|
178
|
+
credentials = {}
|
|
179
|
+
if credentials:
|
|
180
|
+
try:
|
|
181
|
+
bridge = GatewayBridge(credentials)
|
|
182
|
+
async with bridge:
|
|
183
|
+
await bridge.revoke()
|
|
184
|
+
except Exception:
|
|
185
|
+
# Local logout must still succeed when the server is offline.
|
|
186
|
+
pass
|
|
187
|
+
removed = clear_gateway_credentials()
|
|
188
|
+
legacy_removed = clear_private_link()
|
|
189
|
+
removed = removed or legacy_removed
|
|
190
|
+
print("ZeCode device access removed." if removed else "ZeCode was not linked.")
|
|
191
|
+
return 0
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _legacy_login() -> int:
|
|
195
|
+
private_link = getpass(
|
|
196
|
+
"Paste the complete owner private link from Discord /mcp-link: ").strip()
|
|
197
|
+
if not private_link.startswith("https://") or "/mcp/private/" not in private_link:
|
|
198
|
+
raise SystemExit("Invalid private link. Paste the complete HTTPS URL.")
|
|
199
|
+
save_private_link(private_link)
|
|
200
|
+
print("Legacy owner link saved. Run `zecode login` to move to Gateway v2.")
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _launch_update() -> int:
|
|
205
|
+
pipx = shutil.which("pipx")
|
|
206
|
+
if not pipx:
|
|
207
|
+
print("ZeCode update requires pipx.", file=sys.stderr)
|
|
208
|
+
print("Install it in PowerShell with:", file=sys.stderr)
|
|
209
|
+
print(" py -m pip install --user pipx", file=sys.stderr)
|
|
210
|
+
print(" py -m pipx ensurepath", file=sys.stderr)
|
|
211
|
+
return 2
|
|
212
|
+
try:
|
|
213
|
+
metadata.version(PYPI_DISTRIBUTION)
|
|
214
|
+
action = "upgrade"
|
|
215
|
+
label = "Updating"
|
|
216
|
+
except metadata.PackageNotFoundError:
|
|
217
|
+
action = "install"
|
|
218
|
+
label = "Migrating this source installation to"
|
|
219
|
+
print("ZeCode update")
|
|
220
|
+
print(f" {label} {PYPI_DISTRIBUTION} through pipx...")
|
|
221
|
+
completed = subprocess.run(
|
|
222
|
+
[pipx, action, PYPI_DISTRIBUTION],
|
|
223
|
+
check=False,
|
|
224
|
+
)
|
|
225
|
+
if completed.returncode:
|
|
226
|
+
print(
|
|
227
|
+
"ZeCode update failed. pipx kept the current installation unchanged.",
|
|
228
|
+
file=sys.stderr,
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
print("ZeCode update completed in this terminal.")
|
|
232
|
+
return int(completed.returncode)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _friendly_error(exc: BaseException) -> str:
|
|
236
|
+
current = exc
|
|
237
|
+
seen: set[int] = set()
|
|
238
|
+
while id(current) not in seen:
|
|
239
|
+
seen.add(id(current))
|
|
240
|
+
nested = getattr(current, "exceptions", None)
|
|
241
|
+
if not nested:
|
|
242
|
+
break
|
|
243
|
+
current = nested[0]
|
|
244
|
+
message = str(current or type(current).__name__).strip()
|
|
245
|
+
try:
|
|
246
|
+
credentials = load_gateway_credentials()
|
|
247
|
+
except RuntimeError:
|
|
248
|
+
credentials = {}
|
|
249
|
+
for value in (
|
|
250
|
+
credentials.get("access_token"), credentials.get("refresh_token"),
|
|
251
|
+
):
|
|
252
|
+
if value:
|
|
253
|
+
message = message.replace(str(value), "<device-token>")
|
|
254
|
+
try:
|
|
255
|
+
private_link = load_private_link()
|
|
256
|
+
except (OSError, RuntimeError, ValueError):
|
|
257
|
+
private_link = ""
|
|
258
|
+
if private_link:
|
|
259
|
+
message = message.replace(private_link, "<private-link>")
|
|
260
|
+
return message[:800] or type(current).__name__
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def main() -> int:
|
|
264
|
+
args = _parser().parse_args()
|
|
265
|
+
try:
|
|
266
|
+
if args.update_requested:
|
|
267
|
+
return _launch_update()
|
|
268
|
+
if args.command == "login":
|
|
269
|
+
return asyncio.run(_login(args.gateway, args.device_name))
|
|
270
|
+
if args.command == "logout":
|
|
271
|
+
return asyncio.run(_logout())
|
|
272
|
+
if args.command == "doctor":
|
|
273
|
+
return asyncio.run(_doctor(args.gateway))
|
|
274
|
+
if args.command == "legacy-login":
|
|
275
|
+
return _legacy_login()
|
|
276
|
+
if args.command == "update":
|
|
277
|
+
return _launch_update()
|
|
278
|
+
return asyncio.run(_start(args.workspace))
|
|
279
|
+
except KeyboardInterrupt:
|
|
280
|
+
return 130
|
|
281
|
+
except Exception as exc:
|
|
282
|
+
print("ZeCode could not start: " + _friendly_error(exc), file=sys.stderr)
|
|
283
|
+
return 1
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == "__main__":
|
|
287
|
+
raise SystemExit(main())
|