witbitz-code 1.6.8__tar.gz → 1.6.10__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.
- {witbitz_code-1.6.8/src/witbitz_code.egg-info → witbitz_code-1.6.10}/PKG-INFO +2 -1
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/pyproject.toml +3 -1
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/__init__.py +1 -1
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/confidential.py +7 -1
- witbitz_code-1.6.10/src/witbitz_code/images.py +125 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/uninstall.py +25 -6
- {witbitz_code-1.6.8 → witbitz_code-1.6.10/src/witbitz_code.egg-info}/PKG-INFO +2 -1
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code.egg-info/SOURCES.txt +2 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code.egg-info/requires.txt +1 -0
- witbitz_code-1.6.10/tests/test_images.py +102 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_uninstall.py +25 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/LICENSE +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/README.md +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/setup.cfg +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/__main__.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/_js.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/account.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/asks.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/attachments.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/auto.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/auto_runner.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/cli.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/codekeys.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/codetools.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/compress.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/computers.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/confidential_models.json +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/connector.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/devicelink.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/dirs.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/folders.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/gateway.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/icons/witbitz-code-wsl.ico +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/icons/witbitz-code.ico +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/keys.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/link.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/mkdir.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/models.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/net.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/offered_models.json +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/opencode_plugins/notes-init.md +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/opencode_plugins/witbitz-media-server.mjs +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/opencode_plugins/witbitz-notes.js +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/opencode_plugins/witbitz-progress.js +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/outputs.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/pair.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/pairings.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/payers.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/plugins.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/policy.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/ports.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/qr.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/receipt.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/relay.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/seen.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/tinfoil_read.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/tools_catalog.json +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/tools_probe.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/toolsetup.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/ui.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/ui_static/app.css +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/ui_static/app.js +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/ui_static/index.html +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/uitools.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/winagent.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/winbg.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/winconsole.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/wizard.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/wslstart.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code.egg-info/dependency_links.txt +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code.egg-info/entry_points.txt +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code.egg-info/top_level.txt +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_asks.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_attachments.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_auto.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_cli.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_codekeys.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_codetools.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_confidential.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_connector_e2e.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_devicelink.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_dirs.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_files_and_compress.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_folders.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_outputs.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_pair_flows.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_plugins.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_policy.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_ports.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_ports_win.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_registry.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_relay_codec.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_setup.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_tools_probe.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_toolsetup.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_ui.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_uitools.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_winagent.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_winbg.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_winconsole.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_windows_files.py +0 -0
- {witbitz_code-1.6.8 → witbitz_code-1.6.10}/tests/test_wslstart.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: witbitz-code
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.10
|
|
4
4
|
Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
|
|
5
5
|
Author: Witbitz
|
|
6
6
|
License-Expression: MIT
|
|
@@ -19,6 +19,7 @@ Requires-Dist: websockets>=13
|
|
|
19
19
|
Requires-Dist: httpx>=0.25
|
|
20
20
|
Requires-Dist: segno>=1.5
|
|
21
21
|
Requires-Dist: tinfoil<0.15,>=0.14
|
|
22
|
+
Requires-Dist: pillow>=10
|
|
22
23
|
Provides-Extra: test
|
|
23
24
|
Requires-Dist: pytest>=8; extra == "test"
|
|
24
25
|
Requires-Dist: pyflakes>=3; extra == "test"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "witbitz-code"
|
|
7
|
-
version = "1.6.
|
|
7
|
+
version = "1.6.10"
|
|
8
8
|
description = "Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -26,6 +26,8 @@ dependencies = [
|
|
|
26
26
|
# Tinfoil's own verifier (attestation + Sigstore + TLS pinning) — the confidential proxy reads images and files a
|
|
27
27
|
# text-only confidential model cannot, in Tinfoil's attested enclave (tinfoil_read.py). Pinned to a series on purpose.
|
|
28
28
|
"tinfoil>=0.14,<0.15",
|
|
29
|
+
# Older screenshots are re-encoded smaller before a request leaves (images.py): TrustedRouter drops a ~10 MB request.
|
|
30
|
+
"pillow>=10",
|
|
29
31
|
]
|
|
30
32
|
|
|
31
33
|
[project.optional-dependencies]
|
|
@@ -26,7 +26,7 @@ from typing import Any, Callable
|
|
|
26
26
|
|
|
27
27
|
import httpx
|
|
28
28
|
|
|
29
|
-
from . import keys, models as mdl, receipt as rcp, tinfoil_read
|
|
29
|
+
from . import images, keys, models as mdl, receipt as rcp, tinfoil_read
|
|
30
30
|
from .gateway import verify_gateway_attestation
|
|
31
31
|
|
|
32
32
|
TR_UPSTREAM = "https://api.trustedrouter.com"
|
|
@@ -577,6 +577,12 @@ class ConfidentialProxy:
|
|
|
577
577
|
return
|
|
578
578
|
req.headers = [(k, v) for k, v in req.headers if k.lower() not in ("authorization", "x-api-key")] + [("authorization", "Bearer " + cred["trustedrouter"])]
|
|
579
579
|
tinfoil = cred.get("tinfoil") or ""
|
|
580
|
+
if isinstance(body, dict):
|
|
581
|
+
shrunk, rep = await asyncio.to_thread(images.shrink_older_images, body)
|
|
582
|
+
if shrunk is not body:
|
|
583
|
+
body, req.body = shrunk, dumps(shrunk).encode("utf-8")
|
|
584
|
+
if rep["dropped"]:
|
|
585
|
+
self.log(f"code-confidential: the request was {rep['before'] // 1024} KB; {rep['dropped']} older screenshot(s) left out to send it")
|
|
580
586
|
conf = self.models.get(body.get("model")) if isinstance(body, dict) and isinstance(body.get("model"), str) else None
|
|
581
587
|
if not conf:
|
|
582
588
|
await self._pass_through(req, res, body, target)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""OLDER IMAGES ARE SHRUNK BEFORE A REQUEST LEAVES THIS COMPUTER.
|
|
2
|
+
|
|
3
|
+
OpenCode sends the whole conversation on every call, and every screenshot in it rides along as a full base64 PNG. A model
|
|
4
|
+
reads an image as a fixed number of tokens whatever its bytes, so the context stays small while the REQUEST grows with each
|
|
5
|
+
screenshot — and TrustedRouter drops a request of about 10 MB without answering (measured 2026-10-12: 9 MB answered,
|
|
6
|
+
10 MB closed the connection; a session with 18 screenshots reached 11 MB and every message in it failed).
|
|
7
|
+
|
|
8
|
+
So: the newest KEEP_FULL images go as they are; every older one is re-encoded as a JPEG no larger than MAX_SIDE on its
|
|
9
|
+
longest side. A screenshot of ~1 MB becomes ~100–200 KB and still reads. The result is cached by content, so the same old
|
|
10
|
+
screenshot shrinks to the same bytes on every call (a provider's prompt cache keeps working from there on). If the request
|
|
11
|
+
is STILL over BUDGET, the oldest images are replaced by a one-line note, newest kept — a request that cannot be sent helps
|
|
12
|
+
no one. No image library, or an image it cannot decode: that image is left as it is (the budget rule still applies).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import base64
|
|
18
|
+
import hashlib
|
|
19
|
+
import io
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
from collections import OrderedDict
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
KEEP_FULL = 3
|
|
26
|
+
MAX_SIDE = 1280
|
|
27
|
+
JPEG_QUALITY = 70
|
|
28
|
+
BUDGET = 8 * 1024 * 1024 # bytes of request body; TrustedRouter's limit is ~10 MB
|
|
29
|
+
CACHE_MAX = 256
|
|
30
|
+
_DATA_URL = re.compile(r"^data:([^;,]+)?(?:;[^,]*)?;base64,(.*)$", re.S)
|
|
31
|
+
|
|
32
|
+
_cache: "OrderedDict[str, str | None]" = OrderedDict()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _image_parts(body: dict) -> list[tuple[int, int]]:
|
|
36
|
+
"""(message, part) of every inline image, oldest first."""
|
|
37
|
+
out = []
|
|
38
|
+
for mi, msg in enumerate(body.get("messages") or []):
|
|
39
|
+
content = msg.get("content") if isinstance(msg, dict) else None
|
|
40
|
+
if not isinstance(content, list):
|
|
41
|
+
continue
|
|
42
|
+
for pi, part in enumerate(content):
|
|
43
|
+
if isinstance(part, dict) and part.get("type") == "image_url" and _DATA_URL.match(str((part.get("image_url") or {}).get("url") or "")):
|
|
44
|
+
out.append((mi, pi))
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def shrink_data_url(url: str) -> str | None:
|
|
49
|
+
"""A smaller JPEG data URL for this image, or None (no library, cannot decode, or not smaller)."""
|
|
50
|
+
key = hashlib.sha256(url.encode("utf-8", "replace")).hexdigest()
|
|
51
|
+
if key in _cache:
|
|
52
|
+
_cache.move_to_end(key)
|
|
53
|
+
return _cache[key]
|
|
54
|
+
out = None
|
|
55
|
+
try:
|
|
56
|
+
from PIL import Image # optional: without it, images go as they are
|
|
57
|
+
m = _DATA_URL.match(url)
|
|
58
|
+
raw = base64.b64decode(m.group(2), validate=False)
|
|
59
|
+
with Image.open(io.BytesIO(raw)) as im:
|
|
60
|
+
im.load()
|
|
61
|
+
im = im.convert("RGBA") if im.mode in ("P", "LA") else im
|
|
62
|
+
if im.mode == "RGBA":
|
|
63
|
+
bg = Image.new("RGB", im.size, (255, 255, 255))
|
|
64
|
+
bg.paste(im, mask=im.split()[3])
|
|
65
|
+
im = bg
|
|
66
|
+
elif im.mode != "RGB":
|
|
67
|
+
im = im.convert("RGB")
|
|
68
|
+
if max(im.size) > MAX_SIDE:
|
|
69
|
+
im.thumbnail((MAX_SIDE, MAX_SIDE), Image.LANCZOS)
|
|
70
|
+
buf = io.BytesIO()
|
|
71
|
+
im.save(buf, "JPEG", quality=JPEG_QUALITY, optimize=True)
|
|
72
|
+
small = "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode("ascii")
|
|
73
|
+
out = small if len(small) < len(url) else None
|
|
74
|
+
except Exception: # noqa: BLE001 — an image we cannot shrink is sent as it is
|
|
75
|
+
out = None
|
|
76
|
+
_cache[key] = out
|
|
77
|
+
while len(_cache) > CACHE_MAX:
|
|
78
|
+
_cache.popitem(last=False)
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _size(body: dict) -> int:
|
|
83
|
+
return len(json.dumps(body, separators=(",", ":"), ensure_ascii=False).encode("utf-8"))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def shrink_older_images(body: Any, keep_full: int = KEEP_FULL, budget: int = BUDGET) -> tuple[Any, dict]:
|
|
87
|
+
"""(body, report). The body is a NEW dict when anything changed; the input is never mutated."""
|
|
88
|
+
report = {"shrunk": 0, "dropped": 0, "before": 0, "after": 0}
|
|
89
|
+
if not isinstance(body, dict):
|
|
90
|
+
return body, report
|
|
91
|
+
parts = _image_parts(body)
|
|
92
|
+
if not parts:
|
|
93
|
+
return body, report
|
|
94
|
+
messages = list(body["messages"])
|
|
95
|
+
touched: set[int] = set()
|
|
96
|
+
|
|
97
|
+
def put(mi: int, pi: int, part: dict) -> None:
|
|
98
|
+
if mi not in touched:
|
|
99
|
+
messages[mi] = {**messages[mi], "content": list(messages[mi]["content"])}
|
|
100
|
+
touched.add(mi)
|
|
101
|
+
messages[mi]["content"][pi] = part
|
|
102
|
+
|
|
103
|
+
older = parts[:-keep_full] if keep_full > 0 else parts
|
|
104
|
+
for mi, pi in older:
|
|
105
|
+
part = messages[mi]["content"][pi]
|
|
106
|
+
small = shrink_data_url(part["image_url"]["url"])
|
|
107
|
+
if small:
|
|
108
|
+
put(mi, pi, {**part, "image_url": {**part["image_url"], "url": small}})
|
|
109
|
+
report["shrunk"] += 1
|
|
110
|
+
out = {**body, "messages": messages} if touched else body
|
|
111
|
+
size = _size(out)
|
|
112
|
+
if size > budget:
|
|
113
|
+
report["before"] = size
|
|
114
|
+
for mi, pi in parts[:-1]: # the newest image is never dropped
|
|
115
|
+
if size <= budget:
|
|
116
|
+
break
|
|
117
|
+
part = messages[mi]["content"][pi]
|
|
118
|
+
was = len(json.dumps(part, separators=(",", ":"), ensure_ascii=False).encode("utf-8"))
|
|
119
|
+
note = {"type": "text", "text": "[An earlier screenshot was removed here to keep the request small enough to send.]"}
|
|
120
|
+
put(mi, pi, note)
|
|
121
|
+
size -= was - len(json.dumps(note, separators=(",", ":")).encode("utf-8"))
|
|
122
|
+
report["dropped"] += 1
|
|
123
|
+
out = {**body, "messages": messages}
|
|
124
|
+
report["after"] = size
|
|
125
|
+
return out, report
|
|
@@ -302,11 +302,26 @@ def remove_opencode_program(info: dict) -> dict:
|
|
|
302
302
|
return {"ok": False, "why": f"{info['path']} is still there", "said": said} if os.path.exists(info["path"]) else {"ok": True, "said": said}
|
|
303
303
|
|
|
304
304
|
|
|
305
|
+
WINDOWS_REINSTALL = "irm https://app.witbitz.chat/code.ps1 | iex"
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def windows_installer_root() -> str:
|
|
309
|
+
"""%LOCALAPPDATA%\\Witbitz — where the Windows installer (code.ps1) puts this program, OpenCode and the uninstaller."""
|
|
310
|
+
base = os.environ.get("LOCALAPPDATA") or ""
|
|
311
|
+
return (base.rstrip("\\/") + "\\Witbitz") if base else ""
|
|
312
|
+
|
|
313
|
+
|
|
305
314
|
def pipx_install() -> dict:
|
|
306
|
-
"""This package as pipx installed it → {"kind": "pipx", "venv"} (removable with `pipx uninstall`)
|
|
315
|
+
"""This package as pipx installed it → {"kind": "pipx", "venv"} (removable with `pipx uninstall`); as the Windows
|
|
316
|
+
installer (code.ps1) installed it → {"kind": "windows-installer"} (its own uninstaller removes the program, OpenCode
|
|
317
|
+
and the folder right after this — recognised by the flag it sets, or by where the program lives, so an uninstaller
|
|
318
|
+
written by an older install is covered too); else pip / other."""
|
|
307
319
|
prefix = Path(sys.prefix)
|
|
308
320
|
if (prefix / "pipx_metadata.json").exists(): # pipx writes it into every venv it makes, wherever PIPX_HOME is
|
|
309
321
|
return {"kind": "pipx", "venv": str(prefix)}
|
|
322
|
+
root = windows_installer_root()
|
|
323
|
+
if os.environ.get("WITBITZ_INSTALLER") == "windows" or (root and str(sys.prefix).lower().startswith(root.lower() + "\\")):
|
|
324
|
+
return {"kind": "windows-installer"}
|
|
310
325
|
return {"kind": "pip", "hint": f"{Path(sys.executable).name} -m pip uninstall witbitz-code"}
|
|
311
326
|
|
|
312
327
|
|
|
@@ -364,9 +379,12 @@ async def run_uninstall(d: Deps) -> dict:
|
|
|
364
379
|
d.say(f" • delete {d.code_dir} (the pairing secrets, files you attached in Code, the Auto-mode log)")
|
|
365
380
|
if owned:
|
|
366
381
|
d.say(f" • remove what witbitz-code added to OpenCode ({len(owned)} item{'s' if len(owned) > 1 else ''}: the project-notes and progress plugins, the media and code tools)")
|
|
382
|
+
winst = pkg["kind"] == "windows-installer"
|
|
367
383
|
if pkg["kind"] == "pipx":
|
|
368
384
|
d.say(" • uninstall the witbitz-code program (pipx uninstall witbitz-code)")
|
|
369
|
-
|
|
385
|
+
if winst:
|
|
386
|
+
d.say(" • then the Windows uninstaller removes the program, OpenCode and the Witbitz folder")
|
|
387
|
+
d.say(("It keeps your projects" if winst else "It keeps OpenCode and your projects") + " — and, unless you say so next, your keys, notes and sessions.\n")
|
|
370
388
|
if not d.yes and not d.ask("Continue? [y/N] ").strip().lower().startswith("y"):
|
|
371
389
|
d.say("Nothing changed.")
|
|
372
390
|
return {"done": False}
|
|
@@ -469,21 +487,22 @@ async def run_uninstall(d: Deps) -> dict:
|
|
|
469
487
|
pkg_gone = bool(r.get("ok"))
|
|
470
488
|
d.say("✓ Uninstalled the witbitz-code program" if pkg_gone else f"✖ Could not uninstall the program ({r.get('why')}) — run: pipx uninstall witbitz-code")
|
|
471
489
|
d.say("")
|
|
472
|
-
|
|
490
|
+
winst = pkg["kind"] == "windows-installer"
|
|
491
|
+
if pairs and not winst: # the Windows uninstaller stops whatever still runs from its folder
|
|
473
492
|
d.say("If witbitz-code serve is still running in a terminal window, close that window (Ctrl-C).")
|
|
474
493
|
if left:
|
|
475
494
|
d.say(f"Your phone may still list {', '.join(chr(34) + str(p.get('name')) + chr(34) for p in left)}: open Code → Settings → Remove there"
|
|
476
495
|
" — that also cancels its coding keys.")
|
|
477
|
-
if pkg["kind"]
|
|
496
|
+
if pkg["kind"] not in ("pipx", "windows-installer"):
|
|
478
497
|
d.say(f"To remove the program itself: {pkg.get('hint')}")
|
|
479
498
|
if oc_gone:
|
|
480
499
|
tail = "" if remove_sessions else f"\nOpenCode's own data stays: {sessions['dir']} (sessions, saved logins) and its settings in ~/.config/opencode — delete those folders to remove everything."
|
|
481
500
|
d.say(f"witbitz-code and OpenCode are removed. Open a new terminal window so it forgets the old PATH.{tail}")
|
|
482
|
-
elif oc["kind"] != "none":
|
|
501
|
+
elif oc["kind"] != "none" and not winst:
|
|
483
502
|
d.say(f"witbitz-code is removed. OpenCode is still installed — to remove it too: {oc.get('hint')}")
|
|
484
503
|
else:
|
|
485
504
|
d.say("witbitz-code is removed.")
|
|
486
|
-
d.say("To start again: pipx install witbitz-code, then witbitz-code setup.")
|
|
505
|
+
d.say(f"To install again: {WINDOWS_REINSTALL}" if winst else "To start again: pipx install witbitz-code, then witbitz-code setup.")
|
|
487
506
|
return {"done": True, "left": len(left), "package": pkg_gone}
|
|
488
507
|
|
|
489
508
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: witbitz-code
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.10
|
|
4
4
|
Summary: Reach OpenCode on this computer from the Witbitz Spaces Code section, through an end-to-end sealed relay.
|
|
5
5
|
Author: Witbitz
|
|
6
6
|
License-Expression: MIT
|
|
@@ -19,6 +19,7 @@ Requires-Dist: websockets>=13
|
|
|
19
19
|
Requires-Dist: httpx>=0.25
|
|
20
20
|
Requires-Dist: segno>=1.5
|
|
21
21
|
Requires-Dist: tinfoil<0.15,>=0.14
|
|
22
|
+
Requires-Dist: pillow>=10
|
|
22
23
|
Provides-Extra: test
|
|
23
24
|
Requires-Dist: pytest>=8; extra == "test"
|
|
24
25
|
Requires-Dist: pyflakes>=3; extra == "test"
|
|
@@ -21,6 +21,7 @@ src/witbitz_code/devicelink.py
|
|
|
21
21
|
src/witbitz_code/dirs.py
|
|
22
22
|
src/witbitz_code/folders.py
|
|
23
23
|
src/witbitz_code/gateway.py
|
|
24
|
+
src/witbitz_code/images.py
|
|
24
25
|
src/witbitz_code/keys.py
|
|
25
26
|
src/witbitz_code/link.py
|
|
26
27
|
src/witbitz_code/mkdir.py
|
|
@@ -77,6 +78,7 @@ tests/test_devicelink.py
|
|
|
77
78
|
tests/test_dirs.py
|
|
78
79
|
tests/test_files_and_compress.py
|
|
79
80
|
tests/test_folders.py
|
|
81
|
+
tests/test_images.py
|
|
80
82
|
tests/test_outputs.py
|
|
81
83
|
tests/test_pair_flows.py
|
|
82
84
|
tests/test_plugins.py
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Older screenshots are shrunk before a request leaves (images.py) — a session with 18 full PNGs reached 11 MB and
|
|
2
|
+
TrustedRouter dropped every call in it without answering."""
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from PIL import Image
|
|
10
|
+
|
|
11
|
+
from witbitz_code import images
|
|
12
|
+
from test_confidential import needs_fixture, with_proxy
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def png_url(seed: int, side: int = 1800) -> str:
|
|
16
|
+
"""A noisy PNG (noise does not compress, like a real screenshot full of text)."""
|
|
17
|
+
im = Image.frombytes("RGB", (side, side * 2 // 3), os.urandom(side * (side * 2 // 3) * 3) if seed >= 0 else b"\0" * (side * (side * 2 // 3) * 3))
|
|
18
|
+
buf = io.BytesIO()
|
|
19
|
+
im.save(buf, "PNG")
|
|
20
|
+
return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def body_with(urls):
|
|
24
|
+
msgs = [{"role": "system", "content": "be useful"}]
|
|
25
|
+
for i, u in enumerate(urls):
|
|
26
|
+
msgs.append({"role": "user", "content": [{"type": "text", "text": f"shot {i}"}, {"type": "image_url", "image_url": {"url": u}}]})
|
|
27
|
+
msgs.append({"role": "user", "content": "Hi"})
|
|
28
|
+
return {"model": "openai/gpt-6-sol", "messages": msgs, "stream": True}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def urls_of(body):
|
|
32
|
+
return [p["image_url"]["url"] for m in body["messages"] if isinstance(m.get("content"), list) for p in m["content"] if p.get("type") == "image_url"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_the_newest_images_go_as_they_are_and_older_ones_become_small_jpegs():
|
|
36
|
+
urls = [png_url(i, 900) for i in range(5)]
|
|
37
|
+
body = body_with(urls)
|
|
38
|
+
out, rep = images.shrink_older_images(body, keep_full=3)
|
|
39
|
+
got = urls_of(out)
|
|
40
|
+
assert got[2:] == urls[2:], "the newest three are untouched"
|
|
41
|
+
assert all(u.startswith("data:image/jpeg;base64,") for u in got[:2]) and rep["shrunk"] == 2
|
|
42
|
+
assert all(len(a) < len(b) for a, b in zip(got[:2], urls[:2]))
|
|
43
|
+
assert urls_of(body) == urls, "the input is never changed"
|
|
44
|
+
Image.open(io.BytesIO(base64.b64decode(got[0].split(",", 1)[1]))).verify()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_a_large_screenshot_is_brought_down_to_max_side():
|
|
48
|
+
out, _ = images.shrink_older_images(body_with([png_url(1, 2400), png_url(2, 300)]), keep_full=1)
|
|
49
|
+
with Image.open(io.BytesIO(base64.b64decode(urls_of(out)[0].split(",", 1)[1]))) as im:
|
|
50
|
+
assert max(im.size) == images.MAX_SIDE
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_the_same_old_image_shrinks_to_the_same_bytes_every_call():
|
|
54
|
+
urls = [png_url(i, 700) for i in range(4)]
|
|
55
|
+
a, _ = images.shrink_older_images(body_with(urls), keep_full=1)
|
|
56
|
+
images._cache.clear()
|
|
57
|
+
b, _ = images.shrink_older_images(body_with(urls), keep_full=1)
|
|
58
|
+
assert urls_of(a) == urls_of(b), "deterministic, so a provider's prompt cache keeps working"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_no_images_or_only_new_ones_leave_the_body_as_it_was():
|
|
62
|
+
plain = {"model": "m", "messages": [{"role": "user", "content": "hi"}]}
|
|
63
|
+
assert images.shrink_older_images(plain)[0] is plain
|
|
64
|
+
few = body_with([png_url(1, 300)])
|
|
65
|
+
assert images.shrink_older_images(few)[0] is few
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_an_image_it_cannot_decode_is_sent_as_it_is():
|
|
69
|
+
bad = "data:image/png;base64," + base64.b64encode(b"not an image" * 50).decode()
|
|
70
|
+
body = body_with([bad, png_url(1, 300)])
|
|
71
|
+
out, rep = images.shrink_older_images(body, keep_full=1)
|
|
72
|
+
assert urls_of(out)[0] == bad and rep["shrunk"] == 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_over_the_budget_the_oldest_images_become_a_note_and_the_newest_stays():
|
|
76
|
+
urls = [png_url(i, 900) for i in range(4)]
|
|
77
|
+
body = body_with(urls)
|
|
78
|
+
out, rep = images.shrink_older_images(body, keep_full=4, budget=len(urls[0]) * 2)
|
|
79
|
+
assert rep["dropped"] >= 1 and urls_of(out)[-1] == urls[-1]
|
|
80
|
+
assert len(json.dumps(out)) <= len(urls[0]) * 2 + 2000
|
|
81
|
+
assert any(p.get("type") == "text" and "screenshot was removed" in p["text"] for m in out["messages"] if isinstance(m["content"], list) for p in m["content"])
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_the_owners_session_shape_fits_under_trustedrouters_limit():
|
|
85
|
+
"""18 screenshots of ~0.6 MB: 11 MB before; now well under the 10 MB TrustedRouter takes."""
|
|
86
|
+
urls = [png_url(i, 1100) for i in range(18)]
|
|
87
|
+
body = body_with(urls)
|
|
88
|
+
before = len(json.dumps(body))
|
|
89
|
+
out, _ = images.shrink_older_images(body)
|
|
90
|
+
after = len(json.dumps(out))
|
|
91
|
+
assert before > 10 * 1024 * 1024 and after < images.BUDGET, (before, after)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@needs_fixture
|
|
95
|
+
def test_the_proxy_sends_the_shrunk_body_upstream():
|
|
96
|
+
urls = [png_url(i, 900) for i in range(5)]
|
|
97
|
+
|
|
98
|
+
async def t(post, net, _p):
|
|
99
|
+
await post(body_with(urls))
|
|
100
|
+
sent = urls_of(json.loads(net.calls[0]["body"]))
|
|
101
|
+
assert sent[2:] == urls[2:] and all(u.startswith("data:image/jpeg") for u in sent[:2])
|
|
102
|
+
with_proxy(t)
|
|
@@ -210,3 +210,28 @@ def test_windows_finds_opencode_by_name():
|
|
|
210
210
|
out = '"opencode.exe","19228","Console","1","95,000 K","Running","ALONASUS\\shter","0:00:03","N/A"\n' \
|
|
211
211
|
'"opencode.exe","555","Console","1","1 K","Running","OTHER\\bob","0:00:01","N/A"\n'
|
|
212
212
|
assert un.win_opencode_processes(run=lambda argv: out, user="shter") == [{"pid": 19228, "cmd": "opencode.exe"}]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# THE WINDOWS INSTALLER (code.ps1) — the owner, 2026-10-11, Settings → Apps → Witbitz Code → Uninstall printed pip/pipx advice:
|
|
216
|
+
# "To remove the program itself: python.exe -m pip uninstall witbitz-code", "OpenCode is still installed … remove it the
|
|
217
|
+
# way it was installed", "To start again: pipx install witbitz-code". That uninstaller removes the program, OpenCode (in
|
|
218
|
+
# %LOCALAPPDATA%\Witbitz\bin) and the folder right after this, and stops anything still running from it.
|
|
219
|
+
def test_under_the_windows_installer_the_summary_says_what_that_uninstaller_does_not_pip_or_pipx_advice():
|
|
220
|
+
f = Fake(answers=["y"], pairs=[PAIR], oc="installer", pkg="windows-installer")
|
|
221
|
+
r = run(un.run_uninstall(f.deps(yes=True)))
|
|
222
|
+
said = "\n".join(f.said)
|
|
223
|
+
assert r["done"]
|
|
224
|
+
for wrong in ("pip uninstall", "pipx", "OpenCode is still installed", "still running in a terminal window"):
|
|
225
|
+
assert wrong not in said, f"{wrong!r} does not apply to the Windows installer:\n{said}"
|
|
226
|
+
assert "irm https://app.witbitz.chat/code.ps1 | iex" in said, said
|
|
227
|
+
assert ("package",) not in f.done, "the Windows uninstaller removes the program itself"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_the_windows_installer_is_recognised_by_where_the_program_lives_even_from_an_older_uninstaller(monkeypatch):
|
|
231
|
+
monkeypatch.delenv("WITBITZ_INSTALLER", raising=False)
|
|
232
|
+
monkeypatch.setenv("LOCALAPPDATA", r"C:\Users\shter\AppData\Local")
|
|
233
|
+
monkeypatch.setattr(un.sys, "prefix", r"C:\Users\shter\AppData\Local\Witbitz\venv")
|
|
234
|
+
assert un.pipx_install()["kind"] == "windows-installer"
|
|
235
|
+
monkeypatch.setattr(un.sys, "prefix", "/home/a/.local/pipx/venvs/witbitz-code")
|
|
236
|
+
monkeypatch.setenv("WITBITZ_INSTALLER", "windows")
|
|
237
|
+
assert un.pipx_install()["kind"] == "windows-installer", "or by the flag the new uninstaller sets"
|
|
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
|
|
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
|
|
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
|
{witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/opencode_plugins/witbitz-notes.js
RENAMED
|
File without changes
|
{witbitz_code-1.6.8 → witbitz_code-1.6.10}/src/witbitz_code/opencode_plugins/witbitz-progress.js
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|