rly 0.2.0__py3-none-win_amd64.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.
rly/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """PyPI launcher for the ReplyLayer CLI."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("rly")
rly/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
rly/_binaries/.gitkeep ADDED
@@ -0,0 +1,6 @@
1
+ # Placeholder so rly/_binaries/ exists in source even when empty.
2
+ # The per-platform CI matrix in `.github/workflows/build-cli-binaries.yml`
3
+ # drops the matching `replylayer` (or `replylayer.exe`) binary into this
4
+ # directory before running `python -m build`. The binary is .gitignored
5
+ # (see ../../.gitignore) but Hatch wheel-target `artifacts` ensures it
6
+ # lands in the wheel anyway (see ../../pyproject.toml).
Binary file
rly/cli.py ADDED
@@ -0,0 +1,215 @@
1
+ """Bundled-binary launcher for the ReplyLayer CLI.
2
+
3
+ Resolution order (see plans/cli-bundled-binary-launcher.md §4.3):
4
+
5
+ 1. RLY_FORCE_NPX=1 → legacy `npx replylayer@<pinned>` path (W3 logic,
6
+ RLY_OFFLINE-aware). Reachable BEFORE binary resolution so callers
7
+ on musl/Alpine (where _resolve_bundled_binary returns None) still
8
+ have an out if Node is on PATH.
9
+ 2. Bundled binary at `rly/_binaries/replylayer[.exe]` → direct exec.
10
+ 3. Missing binary (sdist install on unsupported platform OR corrupted
11
+ wheel) → exit 1 with an actionable error.
12
+
13
+ Env-var contract:
14
+
15
+ RLY_TIMEOUT_SECONDS: opt-in subprocess timeout (carry-over from W3).
16
+ 10s default for --help/--version quick commands.
17
+ Unbounded for everything else.
18
+ RLY_FORCE_NPX=1: use the legacy npx fallback (emergency / dev path).
19
+ RLY_OFFLINE=1: no-op for the binary path; in the npx fallback,
20
+ injects `npx --offline` + `npm_config_offline=true`
21
+ (W3 behavior preserved per rev. 5 audit finding 3).
22
+
23
+ This module's `main()` is registered as the `rly` console-script entry
24
+ point via packages/pypi-rly/pyproject.toml [project.scripts].
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import os
30
+ import subprocess
31
+ import sys
32
+ from collections.abc import Sequence
33
+ from importlib.metadata import version
34
+ from importlib.resources import files
35
+
36
+ QUICK_COMMAND_TIMEOUT_SECONDS = 10
37
+ QUICK_COMMAND_ARGS = frozenset({"--help", "-h", "--version", "-V"})
38
+
39
+
40
+ def _binary_name() -> str:
41
+ return "replylayer.exe" if sys.platform == "win32" else "replylayer"
42
+
43
+
44
+ def _resolve_bundled_binary() -> str | None:
45
+ """Return the path to the bundled binary, or None if missing.
46
+
47
+ Missing-bundled-binary cases:
48
+ - sdist install on an unsupported platform (no _binaries/ dir).
49
+ - Editable / source install during local development.
50
+ - Corrupted wheel.
51
+
52
+ Note: this does NOT check the host's libc — on a musl host that
53
+ somehow installed a glibc wheel (e.g. forced via --no-binary), the
54
+ binary file exists but exec'ing it will fail. The launcher returns
55
+ the path here regardless; the caller's subprocess.run() will surface
56
+ the dynamic-loader error and Python exits with the resulting code.
57
+ The fall-clear platform-support message from _no_binary_error() runs
58
+ only when the file is genuinely missing.
59
+ """
60
+ try:
61
+ ref = files("rly._binaries").joinpath(_binary_name())
62
+ # importlib.resources returns a Traversable; for a wheel install,
63
+ # this is a real on-disk path. Defensive: handle both is_file()
64
+ # missing (zip-imported edge case) and a stale .gitkeep-only dir.
65
+ if hasattr(ref, "is_file") and ref.is_file():
66
+ return str(ref)
67
+ except (FileNotFoundError, ModuleNotFoundError):
68
+ return None
69
+ return None
70
+
71
+
72
+ def _resolve_timeout(args: list[str]) -> int | None:
73
+ env_timeout = os.environ.get("RLY_TIMEOUT_SECONDS")
74
+ if env_timeout:
75
+ try:
76
+ return int(env_timeout)
77
+ except ValueError:
78
+ return None
79
+ if any(a in QUICK_COMMAND_ARGS for a in args):
80
+ return QUICK_COMMAND_TIMEOUT_SECONDS
81
+ return None
82
+
83
+
84
+ def _no_binary_error() -> str:
85
+ try:
86
+ pinned = version("rly")
87
+ except Exception:
88
+ pinned = "<unknown>"
89
+ return (
90
+ f"rly: no bundled replylayer binary found for this install.\n"
91
+ f"This usually means a source/sdist install on an unsupported platform.\n"
92
+ f"\n"
93
+ f"Supported platforms (binary wheel published):\n"
94
+ f" - linux x86_64 / aarch64 (glibc 2.28+)\n"
95
+ f" - macOS arm64 / x86_64 (>= 14.0)\n"
96
+ f" - Windows x64 (>= Windows 10 / Server 2016)\n"
97
+ f"\n"
98
+ f"If you have Node 22+ on PATH, you can use the legacy npx path:\n"
99
+ f" RLY_FORCE_NPX=1 rly ...\n"
100
+ f"\n"
101
+ f"To force installation of the binary wheel from PyPI:\n"
102
+ f" pipx install --force --pip-args='--only-binary=:all:' rly=={pinned}\n"
103
+ )
104
+
105
+
106
+ def _run_npx_fallback(args: list[str]) -> int:
107
+ """Emergency fallback to `npx --yes replylayer@<pinned>`.
108
+
109
+ Preserves the W3 hardening (RLY_OFFLINE -> npx --offline +
110
+ npm_config_offline=true) byte-for-byte from the pre-rev-11
111
+ packages/pypi-rly/rly/cli.py:52 (rev. 5 audit finding 3 — the rev. 4
112
+ draft dropped this and lost W3's offline contract).
113
+ """
114
+ import shutil
115
+
116
+ npx = shutil.which("npx")
117
+ if npx is None:
118
+ print(
119
+ "rly: RLY_FORCE_NPX=1 requested but npx is not on PATH. "
120
+ "Install Node.js 22+ with npm, or remove RLY_FORCE_NPX.",
121
+ file=sys.stderr,
122
+ )
123
+ return 127
124
+
125
+ try:
126
+ pinned = version("rly")
127
+ except Exception as exc:
128
+ print(
129
+ f"rly: could not resolve installed package version ({exc}). "
130
+ "Reinstall rly: `pipx reinstall rly`.",
131
+ file=sys.stderr,
132
+ )
133
+ return 1
134
+
135
+ env = os.environ.copy()
136
+ env.setdefault("npm_config_update_notifier", "false")
137
+
138
+ offline = os.environ.get("RLY_OFFLINE") == "1"
139
+ if offline:
140
+ env["npm_config_offline"] = "true"
141
+
142
+ cmd = [npx]
143
+ if offline:
144
+ cmd.append("--offline")
145
+ cmd.extend(["--yes", f"replylayer@{pinned}", *args])
146
+
147
+ timeout_s = _resolve_timeout(args)
148
+ try:
149
+ completed = subprocess.run(cmd, env=env, check=False, timeout=timeout_s)
150
+ return completed.returncode
151
+ except subprocess.TimeoutExpired:
152
+ if offline:
153
+ print(
154
+ f"rly: npx fallback did not complete within {timeout_s}s. "
155
+ f"Offline mode is enabled; replylayer@{pinned} may not be "
156
+ f"cached locally.",
157
+ file=sys.stderr,
158
+ )
159
+ else:
160
+ print(
161
+ f"rly: npx fallback did not complete within {timeout_s}s. "
162
+ "Set RLY_TIMEOUT_SECONDS higher, or remove RLY_FORCE_NPX to "
163
+ "use the bundled binary.",
164
+ file=sys.stderr,
165
+ )
166
+ return 124
167
+ except KeyboardInterrupt:
168
+ return 130
169
+
170
+
171
+ def main(argv: Sequence[str] | None = None) -> int:
172
+ args = list(sys.argv[1:] if argv is None else argv)
173
+
174
+ # Emergency / backward-compat path. Use only if the bundled binary is
175
+ # broken on this platform or missing (sdist install). Honored BEFORE
176
+ # any binary work so callers on Alpine/musl (where the wheel falls
177
+ # back to sdist) still have an escape hatch when Node is on PATH.
178
+ if os.environ.get("RLY_FORCE_NPX") == "1":
179
+ return _run_npx_fallback(args)
180
+
181
+ binary_path = _resolve_bundled_binary()
182
+ if binary_path is None:
183
+ print(_no_binary_error(), file=sys.stderr)
184
+ return 1
185
+
186
+ # exec the binary. subprocess.run on Windows + Unix gives clean
187
+ # signal / exit-code semantics. RLY_TIMEOUT_SECONDS opt-in carries
188
+ # over from W3; bundled-binary exec is local + fast, but callers may
189
+ # bound it (e.g. CI watchdogs).
190
+ timeout_s = _resolve_timeout(args)
191
+ try:
192
+ completed = subprocess.run(
193
+ [binary_path, *args], check=False, timeout=timeout_s
194
+ )
195
+ return completed.returncode
196
+ except subprocess.TimeoutExpired:
197
+ print(
198
+ f"rly: command did not complete within {timeout_s}s. "
199
+ "Set RLY_TIMEOUT_SECONDS higher.",
200
+ file=sys.stderr,
201
+ )
202
+ return 124
203
+ except FileNotFoundError:
204
+ # Binary was resolvable as a path but exec failed (binary deleted
205
+ # between resolve and exec — rare race; or the wheel's binary is
206
+ # for a different libc and the dynamic loader rejected it).
207
+ print(
208
+ f"rly: bundled binary at {binary_path} could not be executed. "
209
+ "The wheel may be for a different platform than the host. "
210
+ "Reinstall with `pipx install --force rly`.",
211
+ file=sys.stderr,
212
+ )
213
+ return 127
214
+ except KeyboardInterrupt:
215
+ return 130
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: rly
3
+ Version: 0.2.0
4
+ Summary: PyPI launcher for the ReplyLayer CLI
5
+ Project-URL: Homepage, https://replylayer.ai
6
+ Project-URL: Repository, https://github.com/replylayer/ReplyLayer
7
+ Project-URL: Issues, https://github.com/replylayer/ReplyLayer/issues
8
+ Author: ReplyLayer
9
+ License-Expression: MIT
10
+ Keywords: agent,ai,cli,email,mailbox,replylayer
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Communications :: Email
18
+ Requires-Python: >=3.10
19
+ Provides-Extra: dev
20
+ Requires-Dist: build==1.2.2.post1; extra == 'dev'
21
+ Requires-Dist: pytest==8.3.4; extra == 'dev'
22
+ Requires-Dist: wheel==0.45.1; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # rly
26
+
27
+ `rly` is the PyPI launcher for the ReplyLayer CLI.
28
+
29
+ ```bash
30
+ pipx install rly
31
+ rly --help
32
+ ```
33
+
34
+ On Debian and Ubuntu systems that enforce PEP 668, plain `pip install rly`
35
+ may fail with `externally-managed-environment`. Use `pipx install rly` for a
36
+ global CLI install, or install inside a virtual environment:
37
+
38
+ ```bash
39
+ python3 -m venv .venv
40
+ . .venv/bin/activate
41
+ pip install rly
42
+ ```
43
+
44
+ The installed `rly` command delegates to the official npm package
45
+ `replylayer`, pinned exactly to the matching launcher version (see
46
+ [Strict pin policy](#strict-pin-policy) below).
47
+
48
+ ## Requirements
49
+
50
+ - Python 3.10+
51
+ - Node.js 22+ with npm/npx on `PATH`
52
+ - pipx for the recommended global install path
53
+
54
+ ## Examples
55
+
56
+ ```bash
57
+ rly auth login
58
+ rly mailbox list
59
+ rly inbox list --mailbox support-bot
60
+ ```
61
+
62
+ For full CLI documentation, see the ReplyLayer repository:
63
+ https://github.com/replylayer/ReplyLayer
64
+
65
+ ## Environment variables
66
+
67
+ ### `RLY_TIMEOUT_SECONDS`
68
+
69
+ Opt-in subprocess timeout, in seconds. When set, every `rly` invocation is
70
+ bounded by `subprocess.run(..., timeout=RLY_TIMEOUT_SECONDS)`; if the
71
+ underlying `npx` process does not exit in time the launcher prints a
72
+ diagnostic to stderr and exits with code `124`.
73
+
74
+ By default there is no timeout, with one narrow exception: the help and
75
+ version short-circuits (`--help`, `-h`, `--version`, `-V`) carry a built-in
76
+ 10-second timeout. Those commands should never need to touch the network
77
+ beyond an initial registry resolve; bounding them lets you diagnose a
78
+ restricted-network sandbox without affecting legitimate long-poll commands
79
+ like `rly inbox wait --timeout 60`.
80
+
81
+ ```bash
82
+ RLY_TIMEOUT_SECONDS=30 rly inbox list --mailbox support-bot
83
+ ```
84
+
85
+ Set the value high enough to cover the slowest command you reasonably
86
+ expect. Setting it to a non-integer string is treated as "no timeout"
87
+ (graceful fallback rather than a launcher crash).
88
+
89
+ ### `RLY_OFFLINE`
90
+
91
+ Set `RLY_OFFLINE=1` to force `npx` to resolve `replylayer` from the local
92
+ cache only. The launcher passes both `--offline` on the command line **and**
93
+ `npm_config_offline=true` in the subprocess environment (belt-and-suspenders;
94
+ `--prefer-offline` is NOT a no-network guarantee).
95
+
96
+ If the pinned `replylayer` version isn't cached locally, `npx` exits fast
97
+ with its own non-zero exit code (typically `1` with `ENOTCACHED`); the
98
+ wrapper passes that exit code through verbatim — it does NOT normalize it
99
+ to `124`. Exit code `124` is reserved for the wrapper's own timeout branch.
100
+
101
+ ```bash
102
+ # Populate the cache once, online:
103
+ rly --version
104
+ # Subsequent calls can run offline:
105
+ RLY_OFFLINE=1 rly --version
106
+ ```
107
+
108
+ `RLY_OFFLINE=1` is useful in network-restricted CI sandboxes, air-gapped
109
+ deployments, or any environment where you want a hard guarantee that the
110
+ launcher will not reach out to the npm registry.
111
+
112
+ ## Strict pin policy
113
+
114
+ `rly@X.Y.Z` always resolves `replylayer@X.Y.Z` exactly — there is no
115
+ `@latest` resolution and no semver range. The launcher computes the pinned
116
+ package string at runtime via `importlib.metadata.version('rly')`, so the
117
+ two artifacts always move together.
118
+
119
+ This means:
120
+
121
+ - A published `rly@X.Y.Z` cannot ride a later patch-level `replylayer`
122
+ release; both packages must be republished together. The matched-pair
123
+ release runbook lives at `docs/runbooks/cli-release.md` in the
124
+ ReplyLayer repository.
125
+ - Source installs from the repository between a launcher-only PR and the
126
+ paired version bump are intentionally unsupported. Use `pipx install rly`
127
+ (PyPI) or `npm install -g replylayer` (npm) as the supported install
128
+ paths.
@@ -0,0 +1,9 @@
1
+ rly/__init__.py,sha256=5FlJ4Z6BHHJ3nif_IiZ5G-hdpUrdA9ZvV1erUDV2hNU,119
2
+ rly/__main__.py,sha256=h1QpgvR08tO6TfQHuySKKgQiQVXVIoQRKLEx5sVht_M,67
3
+ rly/cli.py,sha256=V8PJDwg_FgaMR4fcyh5cRsvzHdYkZfn70UpTkDjEYWc,8178
4
+ rly/_binaries/.gitkeep,sha256=up7lTij0VRXRaKJ2tBBLOYP9RwySj9uovZ-sKttm2Bk,419
5
+ rly/_binaries/replylayer.exe,sha256=ic9ryP5Zf4MGH8i8nMN1KCaI8a4g64lAShV3N4zg6yM,80875520
6
+ rly-0.2.0.dist-info/METADATA,sha256=mi4WiO0CNMtnwsHG15hiWkKxh4lYEaEuRGGCfD_IWVI,4450
7
+ rly-0.2.0.dist-info/WHEEL,sha256=fip2IBhkkNvH-S_Xh3PV94_XVqHJB1nn9gTAcldDBj4,94
8
+ rly-0.2.0.dist-info/entry_points.txt,sha256=WTGfCCCFuHNRnhfvvP2y59WS2siy5SzSmXiaQ8t5sPQ,37
9
+ rly-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-win_amd64
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ rly = rly.cli:main