caurad 0.10.1__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.
- caurad-0.10.1/.gitignore +36 -0
- caurad-0.10.1/PKG-INFO +60 -0
- caurad-0.10.1/README.md +51 -0
- caurad-0.10.1/caurad/__init__.py +1 -0
- caurad-0.10.1/caurad/cli.py +273 -0
- caurad-0.10.1/pyproject.toml +20 -0
caurad-0.10.1/.gitignore
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Go build artifacts
|
|
2
|
+
*.exe
|
|
3
|
+
*.exe~
|
|
4
|
+
*.dll
|
|
5
|
+
*.so
|
|
6
|
+
*.dylib
|
|
7
|
+
*.test
|
|
8
|
+
*.out
|
|
9
|
+
vendor/
|
|
10
|
+
|
|
11
|
+
# Coverage and profiling
|
|
12
|
+
*.coverprofile
|
|
13
|
+
coverage.txt
|
|
14
|
+
*.prof
|
|
15
|
+
cover.out
|
|
16
|
+
|
|
17
|
+
# Editor / IDE
|
|
18
|
+
.vscode/
|
|
19
|
+
.idea/
|
|
20
|
+
*.swp
|
|
21
|
+
*.swo
|
|
22
|
+
|
|
23
|
+
# OS metadata
|
|
24
|
+
.DS_Store
|
|
25
|
+
Thumbs.db
|
|
26
|
+
|
|
27
|
+
# Secrets and local config (no secrets in repo per Constitution)
|
|
28
|
+
.env
|
|
29
|
+
.env.*
|
|
30
|
+
*.local
|
|
31
|
+
/bin/
|
|
32
|
+
|
|
33
|
+
# Python bytecode (uvx wrapper py_compile)
|
|
34
|
+
__pycache__/
|
|
35
|
+
*.pyc
|
|
36
|
+
dist/
|
caurad-0.10.1/PKG-INFO
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: caurad
|
|
3
|
+
Version: 0.10.1
|
|
4
|
+
Summary: uvx/pip wrapper for the Caura broker: downloads the platform binary from the public release mirror, SHA-256-verifies it, and execs it.
|
|
5
|
+
Project-URL: Homepage, https://caura.ai
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# `caurad` — uvx / pip wrapper
|
|
11
|
+
|
|
12
|
+
Run the Caura broker from the Python ecosystem:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uvx caurad --help # run without installing
|
|
16
|
+
pipx install caurad && caurad --help # or install the canonical entry point
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## How it works
|
|
20
|
+
|
|
21
|
+
This package is a thin dispatcher — the broker itself is a single Go binary.
|
|
22
|
+
|
|
23
|
+
On first canonical or compatibility entry-point invocation,
|
|
24
|
+
`wrappers/uvx/caurad/cli.py` detects your
|
|
25
|
+
platform, downloads the matching signed archive from the public release mirror,
|
|
26
|
+
verifies its SHA-256 against the published `checksums.txt`, caches the binary
|
|
27
|
+
under `~/.cache/memclawd/<version>/`, and `os.execv`s it with your arguments —
|
|
28
|
+
so `uvx caurad <args>` behaves like the native `memclaw <args>`. Subsequent <!-- legacy-name-floor: installed entry point -->
|
|
29
|
+
invocations reuse the cached binary.
|
|
30
|
+
|
|
31
|
+
The install itself does NOT prefetch the binary — the download happens on first
|
|
32
|
+
invocation. That keeps the package tiny and works under pipx / sandboxed
|
|
33
|
+
installs where postinstall hooks don't fire.
|
|
34
|
+
|
|
35
|
+
The binary is downloaded from the public release mirror
|
|
36
|
+
(`https://memclaw.net/memclaw/<tag>/`) — the same source the
|
|
37
|
+
`wrappers/curl/install.sh` installer uses, not this repo's GitHub Releases
|
|
38
|
+
(which are private). SHA-256 verification is the integrity gate.
|
|
39
|
+
|
|
40
|
+
## Environment overrides
|
|
41
|
+
|
|
42
|
+
Both are also read under their historical `MEMCLAW_*` spellings<!-- legacy-name-ok: rule 3 dual-read alias -->, which keep
|
|
43
|
+
working indefinitely; the `CAURA_*` name wins when it holds a non-empty value.
|
|
44
|
+
|
|
45
|
+
- `CAURA_BINARY_BASE` — base URL of a release mirror (default
|
|
46
|
+
`https://memclaw.net/memclaw`). Point it at an on-prem gateway's
|
|
47
|
+
`<origin>/memclaw` to install from a private deployment.
|
|
48
|
+
- `CAURA_VERSION` — pin a specific release tag instead of the installed
|
|
49
|
+
package's version.
|
|
50
|
+
|
|
51
|
+
The published package version is set from the release tag by the PyPI-publish
|
|
52
|
+
workflow; the `0.0.0` in `pyproject.toml` is a placeholder for local checkouts.
|
|
53
|
+
|
|
54
|
+
## Compatibility name
|
|
55
|
+
|
|
56
|
+
Existing `pip install memclawd` and `uvx --from memclawd memclaw` commands keep working through <!-- legacy-name-ok: permanent PyPI compatibility alias -->
|
|
57
|
+
the separate compatibility package, which depends on `caurad`. New installation
|
|
58
|
+
instructions should use the canonical name above.
|
|
59
|
+
|
|
60
|
+
Licensed under Apache-2.0, matching the `LICENSE` at the repository root.
|
caurad-0.10.1/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# `caurad` — uvx / pip wrapper
|
|
2
|
+
|
|
3
|
+
Run the Caura broker from the Python ecosystem:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
uvx caurad --help # run without installing
|
|
7
|
+
pipx install caurad && caurad --help # or install the canonical entry point
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## How it works
|
|
11
|
+
|
|
12
|
+
This package is a thin dispatcher — the broker itself is a single Go binary.
|
|
13
|
+
|
|
14
|
+
On first canonical or compatibility entry-point invocation,
|
|
15
|
+
`wrappers/uvx/caurad/cli.py` detects your
|
|
16
|
+
platform, downloads the matching signed archive from the public release mirror,
|
|
17
|
+
verifies its SHA-256 against the published `checksums.txt`, caches the binary
|
|
18
|
+
under `~/.cache/memclawd/<version>/`, and `os.execv`s it with your arguments —
|
|
19
|
+
so `uvx caurad <args>` behaves like the native `memclaw <args>`. Subsequent <!-- legacy-name-floor: installed entry point -->
|
|
20
|
+
invocations reuse the cached binary.
|
|
21
|
+
|
|
22
|
+
The install itself does NOT prefetch the binary — the download happens on first
|
|
23
|
+
invocation. That keeps the package tiny and works under pipx / sandboxed
|
|
24
|
+
installs where postinstall hooks don't fire.
|
|
25
|
+
|
|
26
|
+
The binary is downloaded from the public release mirror
|
|
27
|
+
(`https://memclaw.net/memclaw/<tag>/`) — the same source the
|
|
28
|
+
`wrappers/curl/install.sh` installer uses, not this repo's GitHub Releases
|
|
29
|
+
(which are private). SHA-256 verification is the integrity gate.
|
|
30
|
+
|
|
31
|
+
## Environment overrides
|
|
32
|
+
|
|
33
|
+
Both are also read under their historical `MEMCLAW_*` spellings<!-- legacy-name-ok: rule 3 dual-read alias -->, which keep
|
|
34
|
+
working indefinitely; the `CAURA_*` name wins when it holds a non-empty value.
|
|
35
|
+
|
|
36
|
+
- `CAURA_BINARY_BASE` — base URL of a release mirror (default
|
|
37
|
+
`https://memclaw.net/memclaw`). Point it at an on-prem gateway's
|
|
38
|
+
`<origin>/memclaw` to install from a private deployment.
|
|
39
|
+
- `CAURA_VERSION` — pin a specific release tag instead of the installed
|
|
40
|
+
package's version.
|
|
41
|
+
|
|
42
|
+
The published package version is set from the release tag by the PyPI-publish
|
|
43
|
+
workflow; the `0.0.0` in `pyproject.toml` is a placeholder for local checkouts.
|
|
44
|
+
|
|
45
|
+
## Compatibility name
|
|
46
|
+
|
|
47
|
+
Existing `pip install memclawd` and `uvx --from memclawd memclaw` commands keep working through <!-- legacy-name-ok: permanent PyPI compatibility alias -->
|
|
48
|
+
the separate compatibility package, which depends on `caurad`. New installation
|
|
49
|
+
instructions should use the canonical name above.
|
|
50
|
+
|
|
51
|
+
Licensed under Apache-2.0, matching the `LICENSE` at the repository root.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Python wrapper for downloading and dispatching the Caura broker."""
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Caura broker — uvx/pip wrapper entry point.
|
|
2
|
+
|
|
3
|
+
On first invocation, downloads the platform-specific signed binary
|
|
4
|
+
from the public release mirror, verifies SHA-256 against the
|
|
5
|
+
published checksums, caches the binary under
|
|
6
|
+
``~/.cache/memclawd/<version>/``, and execs it with forwarded args.
|
|
7
|
+
Subsequent invocations reuse the cached binary.
|
|
8
|
+
|
|
9
|
+
The pip / uvx install does NOT prefetch the binary — fetching
|
|
10
|
+
happens on first ``memclaw`` invocation. This keeps the Python
|
|
11
|
+
package small (just this dispatcher) and works under pipx /
|
|
12
|
+
sandboxes where postinstall hooks aren't fired.
|
|
13
|
+
|
|
14
|
+
What this script does NOT do:
|
|
15
|
+
- Auto-start the daemon (operator runs ``memclaw start``).
|
|
16
|
+
- Modify shell rc files.
|
|
17
|
+
- Verify GPG signatures (T118 is the additive layer; SHA-256 is
|
|
18
|
+
baseline).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import hashlib
|
|
24
|
+
import importlib.metadata as md
|
|
25
|
+
import os
|
|
26
|
+
import platform
|
|
27
|
+
import shutil
|
|
28
|
+
import sys
|
|
29
|
+
import tarfile
|
|
30
|
+
import tempfile
|
|
31
|
+
import urllib.request
|
|
32
|
+
import zipfile
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from urllib.parse import urlparse
|
|
35
|
+
|
|
36
|
+
# Both spellings, CAURA_* first. Resolution below is first NON-EMPTY,
|
|
37
|
+
# not first defined: an operator whose environment carries a blank
|
|
38
|
+
# CAURA_BINARY_BASE beside a working old-spelling one would otherwise
|
|
39
|
+
# silently fetch from the public mirror instead of their private
|
|
40
|
+
# deployment. Same contract as wrappers/curl/install.sh and the npx
|
|
41
|
+
# wrapper — one name set, every entry point honors it.
|
|
42
|
+
VERSION_ENV_CAURA = "CAURA_VERSION"
|
|
43
|
+
VERSION_ENV = "MEMCLAW_VERSION" # legacy-name-ok: rule 3 dual-read alias
|
|
44
|
+
BINARY_BASE_ENV_CAURA = "CAURA_BINARY_BASE"
|
|
45
|
+
BINARY_BASE_ENV = "MEMCLAW_BINARY_BASE" # legacy-name-ok: rule 3 dual-read alias
|
|
46
|
+
# Binaries come from the public release mirror, NOT this repo's GitHub
|
|
47
|
+
# Releases: the repo is private, so unauthenticated downloads 404. The
|
|
48
|
+
# mirror layout matches wrappers/curl/install.sh — <base>/<tag>/<archive>
|
|
49
|
+
# — and is owned by publish-mirror.yml. On-prem gateways serve their
|
|
50
|
+
# own mirror at <origin>/memclaw # legacy-name-floor: the served mirror path
|
|
51
|
+
# — set CAURA_BINARY_BASE to install from a private deployment.
|
|
52
|
+
DEFAULT_BINARY_BASE = "https://memclaw.net/memclaw"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def die(msg: str) -> int:
|
|
56
|
+
print(f"memclaw: {msg}", file=sys.stderr)
|
|
57
|
+
return 1
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def package_version() -> str:
|
|
61
|
+
"""Return the broker version this wrapper expects to dispatch.
|
|
62
|
+
|
|
63
|
+
Sourced from the installed package metadata (which the release
|
|
64
|
+
pipeline rewrites per release) unless CAURA_VERSION (or its
|
|
65
|
+
historical spelling) is set. Strips a leading 'v' so the GitHub
|
|
66
|
+
Release tag form converts cleanly to the goreleaser archive's
|
|
67
|
+
version segment.
|
|
68
|
+
"""
|
|
69
|
+
if v := _resolve(VERSION_ENV_CAURA, VERSION_ENV)[0]:
|
|
70
|
+
return v.lstrip("v")
|
|
71
|
+
try:
|
|
72
|
+
v = md.version("caurad")
|
|
73
|
+
return v.lstrip("v")
|
|
74
|
+
except md.PackageNotFoundError:
|
|
75
|
+
return "0.0.0-dev"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def detect_platform() -> tuple[str, str, bool]:
|
|
79
|
+
"""Return (os, arch, exe) tuple matching the goreleaser archive
|
|
80
|
+
naming. ``exe`` is True only on Windows.
|
|
81
|
+
"""
|
|
82
|
+
system = platform.system().lower()
|
|
83
|
+
machine = platform.machine().lower()
|
|
84
|
+
if machine in ("x86_64", "amd64"):
|
|
85
|
+
arch = "x86_64"
|
|
86
|
+
elif machine in ("arm64", "aarch64"):
|
|
87
|
+
arch = "arm64"
|
|
88
|
+
else:
|
|
89
|
+
raise RuntimeError(f"unsupported arch: {machine}")
|
|
90
|
+
if system == "darwin":
|
|
91
|
+
return "darwin", arch, False
|
|
92
|
+
if system == "linux":
|
|
93
|
+
return "linux", arch, False
|
|
94
|
+
if system.startswith(("win", "msys", "cygwin")):
|
|
95
|
+
return "windows", arch, True
|
|
96
|
+
raise RuntimeError(f"unsupported OS: {system}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def archive_url(base: str, version: str, os_name: str, arch: str, exe: bool) -> str:
|
|
100
|
+
"""Construct the mirror archive URL: ``<base>/<tag>/<archive>``.
|
|
101
|
+
|
|
102
|
+
The tag directory carries a leading "v" (``version`` is stripped of
|
|
103
|
+
it); archive names do not. Names follow the goreleaser template.
|
|
104
|
+
"""
|
|
105
|
+
ext = "zip" if exe else "tar.gz"
|
|
106
|
+
return f"{base}/v{version}/memclaw_{version}_{os_name}_{arch}.{ext}"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def checksums_url(base: str, version: str) -> str:
|
|
110
|
+
return f"{base}/v{version}/checksums.txt"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _resolve(caura: str, legacy: str, default: str = "") -> tuple[str, str]:
|
|
114
|
+
"""The first of the two names holding a NON-EMPTY value, and its name.
|
|
115
|
+
|
|
116
|
+
Returning the name is the same reason internal/envname.Lookup does: a
|
|
117
|
+
message naming the variable the operator actually set is actionable, and
|
|
118
|
+
one naming the other spelling sends them to edit something they never
|
|
119
|
+
wrote. Falls back to `default` under `caura`'s name, which is the one to
|
|
120
|
+
teach when neither was set.
|
|
121
|
+
"""
|
|
122
|
+
for name in (caura, legacy):
|
|
123
|
+
if v := os.environ.get(name):
|
|
124
|
+
return v, name
|
|
125
|
+
return default, caura
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _validate_base_url(base: str, name: str) -> None:
|
|
129
|
+
"""Require an https:// binary base.
|
|
130
|
+
|
|
131
|
+
urllib.request.urlopen honors file://, ftp://, plain http://, etc.
|
|
132
|
+
The archive and its checksums.txt are fetched from the same base, so
|
|
133
|
+
the SHA-256 check only detects tampering when TLS authenticates the
|
|
134
|
+
origin — plain http:// would let a network attacker swap both. https
|
|
135
|
+
also matches the npm wrapper (Node's https.get) and the wrapper's
|
|
136
|
+
original always-https download path.
|
|
137
|
+
"""
|
|
138
|
+
scheme = urlparse(base).scheme
|
|
139
|
+
if scheme != "https":
|
|
140
|
+
raise RuntimeError(
|
|
141
|
+
f"{name} must be https, got {scheme!r} in {base!r}"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def download(url: str, dest: Path) -> None:
|
|
146
|
+
"""Stream `url` to `dest`. Raises on non-200."""
|
|
147
|
+
with urllib.request.urlopen(url) as resp: # noqa: S310 — scheme validated (https) in ensure_binary
|
|
148
|
+
if resp.status != 200:
|
|
149
|
+
raise RuntimeError(f"HTTP {resp.status} for {url}")
|
|
150
|
+
with dest.open("wb") as f:
|
|
151
|
+
shutil.copyfileobj(resp, f)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def fetch_text(url: str) -> str:
|
|
155
|
+
with urllib.request.urlopen(url) as resp: # noqa: S310 — scheme validated (https) in ensure_binary
|
|
156
|
+
if resp.status != 200:
|
|
157
|
+
raise RuntimeError(f"HTTP {resp.status} for {url}")
|
|
158
|
+
return resp.read().decode("utf-8")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def sha256_of(path: Path) -> str:
|
|
162
|
+
h = hashlib.sha256()
|
|
163
|
+
with path.open("rb") as f:
|
|
164
|
+
for chunk in iter(lambda: f.read(1 << 20), b""):
|
|
165
|
+
h.update(chunk)
|
|
166
|
+
return h.hexdigest()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def cache_dir(version: str) -> Path:
|
|
170
|
+
base = Path(os.environ.get("XDG_CACHE_HOME", str(Path.home() / ".cache")))
|
|
171
|
+
return base / "memclawd" / version
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _extract_tar_safely(tf: tarfile.TarFile, dest: Path) -> None:
|
|
175
|
+
"""Extract ``tf`` into ``dest``, rejecting members that escape ``dest``.
|
|
176
|
+
|
|
177
|
+
Prefers the stdlib data filter, but the ``filter=`` keyword only exists
|
|
178
|
+
on Python 3.12+ (backported to 3.11.4, alongside ``tarfile.data_filter``
|
|
179
|
+
— so ``hasattr`` feature-detects it exactly). On older interpreters —
|
|
180
|
+
e.g. stock Debian 12 ships 3.11.2 — passing it raises ``TypeError`` and
|
|
181
|
+
the wrapper never installs its binary. There, hand-check each member:
|
|
182
|
+
allow only regular files and directories whose resolved path stays
|
|
183
|
+
inside ``dest`` (rejecting absolute paths, ``..`` traversal, and
|
|
184
|
+
symlink/device members). That is a narrower guard than ``data_filter``,
|
|
185
|
+
which is fine here — the archive is a SHA-256-verified, single-binary
|
|
186
|
+
goreleaser tarball, not arbitrary input.
|
|
187
|
+
"""
|
|
188
|
+
if hasattr(tarfile, "data_filter"):
|
|
189
|
+
tf.extractall(dest, filter="data")
|
|
190
|
+
return
|
|
191
|
+
dest_resolved = dest.resolve()
|
|
192
|
+
for member in tf.getmembers():
|
|
193
|
+
if not (member.isfile() or member.isdir()):
|
|
194
|
+
raise RuntimeError(f"non-regular member in tar archive: {member.name!r}")
|
|
195
|
+
if not (dest / member.name).resolve().is_relative_to(dest_resolved):
|
|
196
|
+
raise RuntimeError(f"unsafe path in tar archive: {member.name!r}")
|
|
197
|
+
tf.extractall(dest) # members validated above
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def ensure_binary(version: str) -> Path:
|
|
201
|
+
"""Return the absolute path to the cached binary, downloading
|
|
202
|
+
if necessary. Raises on any integrity failure.
|
|
203
|
+
"""
|
|
204
|
+
os_name, arch, exe = detect_platform()
|
|
205
|
+
base, base_name = _resolve(
|
|
206
|
+
BINARY_BASE_ENV_CAURA, BINARY_BASE_ENV, DEFAULT_BINARY_BASE
|
|
207
|
+
)
|
|
208
|
+
base = base.rstrip("/")
|
|
209
|
+
_validate_base_url(base, base_name)
|
|
210
|
+
bin_name = "memclaw.exe" if exe else "memclaw"
|
|
211
|
+
cache = cache_dir(version)
|
|
212
|
+
bin_path = cache / bin_name
|
|
213
|
+
if bin_path.exists():
|
|
214
|
+
return bin_path
|
|
215
|
+
|
|
216
|
+
cache.mkdir(parents=True, exist_ok=True)
|
|
217
|
+
arch_name = f"memclaw_{version}_{os_name}_{arch}.{'zip' if exe else 'tar.gz'}"
|
|
218
|
+
archive_url_full = archive_url(base, version, os_name, arch, exe)
|
|
219
|
+
|
|
220
|
+
print(f"memclaw: downloading {archive_url_full}", file=sys.stderr)
|
|
221
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
222
|
+
tmp_dir = Path(tmp)
|
|
223
|
+
archive_path = tmp_dir / arch_name
|
|
224
|
+
download(archive_url_full, archive_path)
|
|
225
|
+
|
|
226
|
+
# Verify SHA-256.
|
|
227
|
+
sums = fetch_text(checksums_url(base, version))
|
|
228
|
+
expected = None
|
|
229
|
+
for line in sums.splitlines():
|
|
230
|
+
parts = line.split()
|
|
231
|
+
if len(parts) >= 2 and parts[-1] == arch_name:
|
|
232
|
+
expected = parts[0]
|
|
233
|
+
break
|
|
234
|
+
if expected is None:
|
|
235
|
+
raise RuntimeError(f"no checksum entry for {arch_name}")
|
|
236
|
+
actual = sha256_of(archive_path)
|
|
237
|
+
if actual != expected:
|
|
238
|
+
raise RuntimeError(
|
|
239
|
+
f"checksum mismatch for {arch_name}: expected={expected} actual={actual}"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Extract.
|
|
243
|
+
if exe:
|
|
244
|
+
with zipfile.ZipFile(archive_path) as zf:
|
|
245
|
+
zf.extractall(tmp_dir)
|
|
246
|
+
else:
|
|
247
|
+
with tarfile.open(archive_path, "r:gz") as tf:
|
|
248
|
+
# Safe extract, guarded for Pythons without the filter kwarg.
|
|
249
|
+
_extract_tar_safely(tf, tmp_dir)
|
|
250
|
+
src = tmp_dir / bin_name
|
|
251
|
+
if not src.exists():
|
|
252
|
+
raise RuntimeError(f"archive did not contain {bin_name}")
|
|
253
|
+
shutil.move(str(src), str(bin_path))
|
|
254
|
+
if not exe:
|
|
255
|
+
bin_path.chmod(0o755)
|
|
256
|
+
return bin_path
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def main() -> int:
|
|
260
|
+
try:
|
|
261
|
+
version = package_version()
|
|
262
|
+
bin_path = ensure_binary(version)
|
|
263
|
+
except (RuntimeError, OSError) as err:
|
|
264
|
+
return die(str(err))
|
|
265
|
+
# os.execv replaces the Python process with the binary, so
|
|
266
|
+
# stdout/stderr/stdin/signals all flow naturally. argv[0] is
|
|
267
|
+
# the binary's own name to match what `exec memclaw ...`
|
|
268
|
+
# would produce.
|
|
269
|
+
os.execv(str(bin_path), [str(bin_path), *sys.argv[1:]])
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
if __name__ == "__main__":
|
|
273
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "caurad"
|
|
3
|
+
version = "0.10.1"
|
|
4
|
+
description = "uvx/pip wrapper for the Caura broker: downloads the platform binary from the public release mirror, SHA-256-verifies it, and execs it."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
|
|
9
|
+
[project.urls]
|
|
10
|
+
Homepage = "https://caura.ai"
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
caurad = "caurad.cli:main"
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["hatchling>=1.27"]
|
|
17
|
+
build-backend = "hatchling.build"
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.wheel]
|
|
20
|
+
packages = ["caurad"]
|